Skip to main content

robonix_cli/
install.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// Package installation (GitHub, local path) into ~/.robonix/packages
3
4use crate::config::Config;
5use crate::database::{PackageDatabase, PackageInfo, PackageSource};
6use crate::manifest;
7use crate::output;
8use anyhow::{Context, Result};
9use std::io::{self, Write};
10use std::path::Path;
11use std::time::SystemTime;
12
13pub struct PackageInstaller {
14    config: Config,
15}
16
17fn normalize_github_url(repo: &str) -> String {
18    let s = repo.trim().trim_end_matches('/');
19    if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("git@") {
20        return s.to_string();
21    }
22    if s.contains('/') && !s.contains(' ') {
23        return format!("https://github.com/{}.git", s.trim_end_matches(".git"));
24    }
25    s.to_string()
26}
27
28impl PackageInstaller {
29    pub fn new(config: Config) -> Self {
30        Self { config }
31    }
32
33    pub fn install_from_github(&self, repo: &str, branch: Option<&str>) -> Result<String> {
34        let clone_url = normalize_github_url(repo);
35        let repo_url = repo.to_string();
36        let repo_name = repo
37            .split('/')
38            .next_back()
39            .context("Invalid GitHub repository format")?
40            .trim_end_matches(".git");
41
42        let temp_path = self
43            .config
44            .package_storage_path
45            .join(format!("{}_temp", repo_name));
46        if temp_path.exists() {
47            std::fs::remove_dir_all(&temp_path)?;
48        }
49
50        let mut repo_builder = git2::build::RepoBuilder::new();
51        if let Some(b) = branch {
52            repo_builder.branch(b);
53        }
54
55        let git_repo = repo_builder
56            .clone(&clone_url, &temp_path)
57            .context("Failed to clone repository")?;
58
59        let head = git_repo.head().context("Failed to get HEAD")?;
60        let commit = head.target().context("Failed to get commit OID")?;
61        let commit_str = commit.to_string();
62
63        let detected = manifest::detect_and_load(&temp_path)?;
64        output::step("Validating", "package manifest");
65        let summary = detected
66            .manifest
67            .validate_and_summarize()
68            .with_context(|| "Invalid robonix package manifest")?;
69        let package_name = summary.name.clone();
70        let version = summary.version.clone();
71        let target_path = self.config.package_storage_path.join(&package_name);
72
73        output::sub_step(&format!("Package: {} {}", package_name, version));
74
75        let db = PackageDatabase::load(&self.config.package_storage_path)?;
76        if let Some(existing) = db.get_package(&package_name) {
77            if !Self::prompt_overwrite(&existing.version, &version, &package_name)? {
78                std::fs::remove_dir_all(&temp_path)?;
79                output::info("Installation cancelled.");
80                std::process::exit(0);
81            }
82            if existing.path.exists() {
83                std::fs::remove_dir_all(&existing.path)?;
84            }
85        }
86
87        if target_path.exists() {
88            std::fs::remove_dir_all(&target_path)?;
89        }
90        std::fs::rename(&temp_path, &target_path)?;
91
92        let manifest_path = target_path.join(
93            detected
94                .path
95                .file_name()
96                .and_then(|n| n.to_str())
97                .unwrap_or(manifest::MANIFEST_FILE),
98        );
99        let package_info = Self::create_package_info(
100            &target_path,
101            &manifest_path,
102            &summary,
103            PackageSource::GitHub {
104                repo: repo_url,
105                branch: branch.map(String::from),
106                commit: commit_str,
107            },
108        )?;
109
110        let mut db = PackageDatabase::load(&self.config.package_storage_path)?;
111        db.add_package(package_info);
112        db.save(&self.config.package_storage_path)?;
113
114        Ok(package_name)
115    }
116
117    pub fn install_from_path(&self, source_path: &Path) -> Result<String> {
118        let source_path = source_path
119            .canonicalize()
120            .with_context(|| format!("Failed to canonicalize: {}", source_path.display()))?;
121
122        let detected = manifest::detect_and_load(&source_path)?;
123        output::step("Validating", "package manifest");
124        let summary = detected
125            .manifest
126            .validate_and_summarize()
127            .with_context(|| "Invalid robonix package manifest")?;
128        let package_name = summary.name.clone();
129        let version = summary.version.clone();
130
131        output::sub_step(&format!("Package: {} {}", package_name, version));
132
133        let db = PackageDatabase::load(&self.config.package_storage_path)?;
134        if let Some(existing) = db.get_package(&package_name) {
135            if !Self::prompt_overwrite(&existing.version, &version, &package_name)? {
136                output::info("Installation cancelled.");
137                std::process::exit(0);
138            }
139            if existing.path.exists() {
140                std::fs::remove_dir_all(&existing.path)?;
141            }
142        }
143
144        let target_path = self.config.package_storage_path.join(&package_name);
145        copy_dir_all(&source_path, &target_path).with_context(|| {
146            format!(
147                "Failed to copy from {} to {}",
148                source_path.display(),
149                target_path.display()
150            )
151        })?;
152
153        let manifest_path = target_path.join(
154            detected
155                .path
156                .file_name()
157                .and_then(|n| n.to_str())
158                .unwrap_or(manifest::MANIFEST_FILE),
159        );
160        let package_info = Self::create_package_info(
161            &target_path,
162            &manifest_path,
163            &summary,
164            PackageSource::Local { path: source_path },
165        )?;
166
167        let mut db = PackageDatabase::load(&self.config.package_storage_path)?;
168        db.add_package(package_info);
169        db.save(&self.config.package_storage_path)?;
170
171        Ok(package_name)
172    }
173
174    pub fn parse_manifest_name(manifest_path: &Path) -> Result<String> {
175        let manifest = manifest::load_from_path(manifest_path)?;
176        Ok(manifest.validate_and_summarize()?.name)
177    }
178
179    fn prompt_overwrite(old_version: &str, new_version: &str, package_name: &str) -> Result<bool> {
180        output::warning(&format!("Package '{}' is already installed", package_name));
181        output::sub_step(&format!("Old: {}  New: {}", old_version, new_version));
182        print!("Overwrite? [y/N]: ");
183        io::stdout().flush()?;
184        let mut input = String::new();
185        io::stdin().read_line(&mut input)?;
186        let answer = input.trim().to_lowercase();
187        Ok(answer == "y" || answer == "yes")
188    }
189
190    pub fn create_package_info(
191        path: &Path,
192        manifest_path: &Path,
193        summary: &crate::manifest::PackageSummary,
194        source: PackageSource,
195    ) -> Result<PackageInfo> {
196        let installed_at = chrono::DateTime::<chrono::Utc>::from(SystemTime::now()).to_rfc3339();
197        Ok(PackageInfo {
198            name: summary.name.clone(),
199            version: summary.version.clone(),
200            path: path.to_path_buf(),
201            manifest_path: manifest_path.to_path_buf(),
202            capabilities: summary.capabilities.clone(),
203            depends: summary.depends.clone(),
204            installed_at,
205            source,
206        })
207    }
208}
209
210fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
211    std::fs::create_dir_all(dst)?;
212    for entry in std::fs::read_dir(src)? {
213        let entry = entry?;
214        let src_path = entry.path();
215        let dst_path = dst.join(entry.file_name());
216        if entry.file_type()?.is_dir() {
217            copy_dir_all(&src_path, &dst_path)?;
218        } else {
219            std::fs::copy(&src_path, &dst_path)?;
220        }
221    }
222    Ok(())
223}