blueprint_engine_core/
package.rs1use 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}