Skip to main content

nap_core/server/
install.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! Lore installer integration
4//!
5//! Integrates the official Lore installer behind `nap install lore`
6//! to download, install, and verify Lore CLI and server binaries.
7
8use anyhow::{Context, Result};
9use std::fs;
10use std::path::Path;
11use std::process::Command;
12use tracing::{error, info};
13
14/// Lore installer for managing Lore CLI and server installation
15pub struct LoreInstaller {
16    install_dir: std::path::PathBuf,
17    repo: String,
18    version: String,
19}
20
21impl LoreInstaller {
22    /// Create a new Lore installer
23    pub fn new(install_dir: &Path) -> Self {
24        Self {
25            install_dir: install_dir.to_path_buf(),
26            repo: "EpicGames/lore".to_string(),
27            version: "latest".to_string(),
28        }
29    }
30
31    /// Set custom repository
32    pub fn with_repo(mut self, repo: &str) -> Self {
33        self.repo = repo.to_string();
34        self
35    }
36
37    /// Set custom version
38    pub fn with_version(mut self, version: &str) -> Self {
39        self.version = version.to_string();
40        self
41    }
42
43    /// Install Lore CLI
44    pub fn install_cli(&self) -> Result<()> {
45        info!(
46            "Installing Lore CLI from {} version {}",
47            self.repo, self.version
48        );
49
50        self.run_install_script(&["--version", &self.version])?;
51
52        info!("Lore CLI installed successfully");
53        Ok(())
54    }
55
56    /// Install Lore server
57    pub fn install_server(&self) -> Result<()> {
58        info!(
59            "Installing Lore server from {} version {}",
60            self.repo, self.version
61        );
62
63        self.run_install_script(&["--server", "--version", &self.version])?;
64
65        info!("Lore server installed successfully");
66        Ok(())
67    }
68
69    /// Install both CLI and server
70    pub fn install_all(&self) -> Result<()> {
71        info!(
72            "Installing Lore CLI and server from {} version {}",
73            self.repo, self.version
74        );
75
76        self.run_install_script(&["--version", &self.version])?;
77
78        info!("Lore CLI and server installed successfully");
79        Ok(())
80    }
81
82    /// Run the official Lore install script
83    fn run_install_script(&self, args: &[&str]) -> Result<()> {
84        let script_url = format!(
85            "https://raw.githubusercontent.com/{}/main/scripts/install.sh",
86            self.repo
87        );
88
89        // Download script
90        let script_path = self.download_script(&script_url)?;
91
92        // Make script executable
93        #[cfg(unix)]
94        {
95            use std::os::unix::fs::PermissionsExt;
96            let mut perms = fs::metadata(&script_path)?.permissions();
97            perms.set_mode(0o755);
98            fs::set_permissions(&script_path, perms)?;
99        }
100
101        // Build command with install directory and other args
102        let mut cmd_args = vec![
103            script_path.to_str().unwrap(),
104            "--install-dir",
105            self.install_dir.to_str().unwrap(),
106        ];
107        cmd_args.extend(args.iter().copied());
108
109        // Execute script
110        let output = Command::new("bash")
111            .args(&cmd_args)
112            .output()
113            .context("Failed to execute Lore install script")?;
114
115        if !output.status.success() {
116            let stderr = String::from_utf8_lossy(&output.stderr);
117            error!("Lore install script failed: {}", stderr);
118            anyhow::bail!("Lore install script failed with status: {}", output.status);
119        }
120
121        // Clean up script
122        fs::remove_file(&script_path)?;
123
124        Ok(())
125    }
126
127    /// Download install script to temporary location
128    fn download_script(&self, url: &str) -> Result<std::path::PathBuf> {
129        let response =
130            reqwest::blocking::get(url).context("Failed to download Lore install script")?;
131
132        if !response.status().is_success() {
133            anyhow::bail!("Failed to download script: HTTP {}", response.status());
134        }
135
136        let script_content = response.text().context("Failed to read script content")?;
137
138        // Write to temporary file
139        let temp_dir = std::env::temp_dir();
140        let script_path = temp_dir.join("lore-install.sh");
141        fs::write(&script_path, script_content).context("Failed to write install script")?;
142
143        Ok(script_path)
144    }
145
146    /// Verify installation
147    pub fn verify_installation(&self) -> Result<VerificationResult> {
148        let cli_installed = self.check_binary("lore");
149        let server_installed = self.check_binary("loreserver");
150
151        let cli_version = if cli_installed {
152            self.get_binary_version("lore").ok()
153        } else {
154            None
155        };
156
157        let server_version = if server_installed {
158            self.get_binary_version("loreserver").ok()
159        } else {
160            None
161        };
162
163        Ok(VerificationResult {
164            cli_installed,
165            cli_version,
166            server_installed,
167            server_version,
168        })
169    }
170
171    /// Check if binary exists and is executable
172    fn check_binary(&self, name: &str) -> bool {
173        let binary_path = self.install_dir.join(name);
174        binary_path.exists() && binary_path.is_file()
175    }
176
177    /// Get version from binary
178    fn get_binary_version(&self, name: &str) -> Result<String> {
179        let binary_path = self.install_dir.join(name);
180        let output = Command::new(&binary_path)
181            .arg("--version")
182            .output()
183            .context(format!("Failed to execute {} --version", name))?;
184
185        if !output.status.success() {
186            anyhow::bail!("{} --version failed", name);
187        }
188
189        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
190    }
191
192    /// Add install directory to PATH
193    pub fn add_to_path(&self) -> Result<()> {
194        let install_dir_str = self
195            .install_dir
196            .to_str()
197            .context("Install directory path is not valid UTF-8")?;
198
199        // Check if already in PATH
200        if let Ok(current_path) = std::env::var("PATH")
201            && current_path.contains(install_dir_str)
202        {
203            info!("Install directory already in PATH");
204            return Ok(());
205        }
206
207        // Add to current process PATH
208        let new_path = format!(
209            "{}:{}",
210            install_dir_str,
211            std::env::var("PATH").unwrap_or_default()
212        );
213        unsafe {
214            std::env::set_var("PATH", &new_path);
215        }
216
217        info!("Added {} to PATH for current process", install_dir_str);
218        Ok(())
219    }
220}
221
222/// Result of installation verification
223#[derive(Debug, Clone)]
224pub struct VerificationResult {
225    pub cli_installed: bool,
226    pub cli_version: Option<String>,
227    pub server_installed: bool,
228    pub server_version: Option<String>,
229}
230
231impl VerificationResult {
232    /// Check if installation is complete
233    pub fn is_complete(&self) -> bool {
234        self.cli_installed && self.server_installed
235    }
236
237    /// Get a human-readable status message
238    pub fn status_message(&self) -> String {
239        let mut parts = vec![];
240
241        if self.cli_installed {
242            parts.push(format!(
243                "Lore CLI installed ({})",
244                self.cli_version.as_deref().unwrap_or("unknown")
245            ));
246        } else {
247            parts.push("Lore CLI not installed".to_string());
248        }
249
250        if self.server_installed {
251            parts.push(format!(
252                "Lore server installed ({})",
253                self.server_version.as_deref().unwrap_or("unknown")
254            ));
255        } else {
256            parts.push("Lore server not installed".to_string());
257        }
258
259        parts.join("; ")
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use tempfile::TempDir;
267
268    #[test]
269    fn test_installer_creation() {
270        let temp_dir = TempDir::new().unwrap();
271        let installer = LoreInstaller::new(temp_dir.path());
272        assert_eq!(installer.repo, "EpicGames/lore");
273        assert_eq!(installer.version, "latest");
274    }
275
276    #[test]
277    fn test_installer_custom_repo() {
278        let temp_dir = TempDir::new().unwrap();
279        let installer = LoreInstaller::new(temp_dir.path())
280            .with_repo("custom/repo")
281            .with_version("v1.0.0");
282        assert_eq!(installer.repo, "custom/repo");
283        assert_eq!(installer.version, "v1.0.0");
284    }
285
286    #[test]
287    fn test_verification_result() {
288        let result = VerificationResult {
289            cli_installed: true,
290            cli_version: Some("0.8.5-nightly".to_string()),
291            server_installed: false,
292            server_version: None,
293        };
294
295        assert!(!result.is_complete());
296        assert!(result.status_message().contains("Lore CLI installed"));
297        assert!(
298            result
299                .status_message()
300                .contains("Lore server not installed")
301        );
302    }
303}