blueprint_engine_core/
package.rs

1use crate::{BlueprintError, Result};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone)]
5pub struct PackageSpec {
6    pub user: String,
7    pub repo: String,
8    pub version: String,
9}
10
11impl PackageSpec {
12    pub fn parse(package: &str) -> Result<Self> {
13        let path = package.strip_prefix('@').unwrap_or(package);
14
15        let (repo_path, version) = if let Some(idx) = path.find('#') {
16            (&path[..idx], Some(&path[idx + 1..]))
17        } else {
18            (path, None)
19        };
20
21        let parts: Vec<&str> = repo_path.splitn(2, '/').collect();
22        if parts.len() != 2 {
23            return Err(BlueprintError::ArgumentError {
24                message: "Invalid package format. Expected @user/repo or @user/repo#version".into(),
25            });
26        }
27
28        Ok(Self {
29            user: parts[0].to_string(),
30            repo: parts[1].to_string(),
31            version: version.unwrap_or("main").to_string(),
32        })
33    }
34
35    pub fn display_name(&self) -> String {
36        format!("@{}/{}#{}", self.user, self.repo, self.version)
37    }
38
39    pub fn dir_name(&self) -> String {
40        format!("{}#{}", self.repo, self.version)
41    }
42}
43
44pub fn find_workspace_root() -> Option<PathBuf> {
45    find_workspace_root_from(std::env::current_dir().ok()?)
46}
47
48pub fn find_workspace_root_from(start: PathBuf) -> Option<PathBuf> {
49    let mut current = start;
50    loop {
51        let bp_toml = current.join("BP.toml");
52        if bp_toml.exists() {
53            return Some(current);
54        }
55        if !current.pop() {
56            break;
57        }
58    }
59    None
60}
61
62pub fn get_packages_dir() -> PathBuf {
63    if let Some(workspace) = find_workspace_root() {
64        workspace.join(".blueprint").join("packages")
65    } else {
66        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
67        PathBuf::from(&home).join(".blueprint").join("packages")
68    }
69}
70
71pub fn get_packages_dir_from(start: Option<PathBuf>) -> PathBuf {
72    let workspace = start.and_then(find_workspace_root_from);
73    if let Some(ws) = workspace {
74        ws.join(".blueprint").join("packages")
75    } else {
76        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
77        PathBuf::from(&home).join(".blueprint").join("packages")
78    }
79}
80
81const DEFAULT_REGISTRY: &str = "https://blueprint.fleetnet.engineering";
82
83pub fn get_registry_url() -> String {
84    std::env::var("BP_REGISTRY").unwrap_or_else(|_| DEFAULT_REGISTRY.to_string())
85}
86
87pub fn fetch_package(spec: &PackageSpec, dest: &PathBuf) -> Result<()> {
88    let registry = get_registry_url();
89    let download_url = format!(
90        "{}/api/v1/packages/{}/{}/{}/download",
91        registry, spec.user, spec.repo, spec.version
92    );
93
94    let output = std::process::Command::new("curl")
95        .args(["-fsSL", "-o", "-", &download_url])
96        .output()
97        .map_err(|e| BlueprintError::IoError {
98            path: download_url.clone(),
99            message: e.to_string(),
100        })?;
101
102    if !output.status.success() {
103        let stderr = String::from_utf8_lossy(&output.stderr);
104        return Err(BlueprintError::IoError {
105            path: download_url,
106            message: format!("Failed to download package: {}", stderr.trim()),
107        });
108    }
109
110    if let Some(parent) = dest.parent() {
111        std::fs::create_dir_all(parent).map_err(|e| BlueprintError::IoError {
112            path: parent.to_string_lossy().to_string(),
113            message: e.to_string(),
114        })?;
115    }
116
117    std::fs::create_dir_all(dest).map_err(|e| BlueprintError::IoError {
118        path: dest.to_string_lossy().to_string(),
119        message: e.to_string(),
120    })?;
121
122    let tar_output = std::process::Command::new("tar")
123        .args(["-xzf", "-", "-C"])
124        .arg(dest)
125        .stdin(std::process::Stdio::piped())
126        .spawn()
127        .and_then(|mut child| {
128            use std::io::Write;
129            if let Some(stdin) = child.stdin.as_mut() {
130                stdin.write_all(&output.stdout)?;
131            }
132            child.wait()
133        })
134        .map_err(|e| BlueprintError::IoError {
135            path: dest.to_string_lossy().to_string(),
136            message: format!("Failed to extract package: {}", e),
137        })?;
138
139    if !tar_output.success() {
140        std::fs::remove_dir_all(dest).ok();
141        return Err(BlueprintError::IoError {
142            path: dest.to_string_lossy().to_string(),
143            message: "Failed to extract package tarball".into(),
144        });
145    }
146
147    Ok(())
148}