zoi_cli/pkg/
repo_install.rs1use crate::pkg::types;
9use anyhow::{Result, anyhow};
10use colored::*;
11use serde::Deserialize;
12use std::env;
13use std::fs;
14use zoi_core::types::SourceType;
15
16#[derive(Debug, Deserialize)]
17struct RepoFile {
18 package: String,
19}
20
21pub fn run(
22 repo_spec: &str,
23 force: bool,
24 all_optional: bool,
25 yes: bool,
26 scope: Option<crate::cli::SetupScope>,
27 plugin_manager: Option<&crate::pkg::plugin::PluginManager>,
28) -> Result<()> {
29 println!(
30 "Installing from git repository: {}",
31 repo_spec.cyan().bold()
32 );
33
34 let (provider, repo_path) = parse_repo_spec(repo_spec)?;
35
36 crate::pkg::utils::confirm_untrusted_source(&SourceType::GitRepo(repo_spec.to_string()), yes)?;
37
38 let repo_file_names = ["zoi.yaml"];
39 let mut repo_file_content: Option<String> = None;
40 let mut used_url = String::new();
41
42 for file_name in &repo_file_names {
43 if let Ok(url) = get_repo_file_url(&provider, &repo_path, file_name) {
44 println!("Attempting to fetch repo config from: {}", url);
45 let client = crate::pkg::utils::get_http_client().ok();
46 if let Some(c) = client
47 && let Ok(content_res) = c.get(&url).send()
48 && content_res.status().is_success()
49 {
50 repo_file_content = Some(content_res.text()?);
51 used_url = url;
52 break;
53 }
54 }
55 }
56
57 let repo_file_content = repo_file_content.ok_or_else(|| {
58 anyhow!("Could not find zoi.yaml in the repository on main/master branches.")
59 })?;
60 println!("Using repo config from: {}", used_url.cyan());
61
62 let repo_file: RepoFile = serde_yaml::from_str(&repo_file_content)?;
63
64 let package_source = &repo_file.package;
65
66 let scope_override = scope.map(|s| match s {
67 crate::cli::SetupScope::User => types::Scope::User,
68 crate::cli::SetupScope::System => types::Scope::System,
69 });
70
71 println!("Starting installation of package from git repo...");
72
73 let source_to_install = if package_source.starts_with("http") {
74 println!("Package source is a URL: {}", package_source.cyan());
75 let client = crate::pkg::utils::get_http_client()?;
76 let pkg_content = client.get(package_source).send()?.text()?;
77 let temp_path = env::temp_dir().join(format!(
78 "zoi-repo-install-{}.pkg.lua",
79 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
80 ));
81 fs::write(&temp_path, pkg_content)?;
82 temp_path
83 .to_str()
84 .ok_or_else(|| anyhow!("Temporary path contains invalid UTF-8"))?
85 .to_string()
86 } else if package_source.ends_with(".pkg.lua")
87 || (package_source.contains('/') && !package_source.starts_with('@'))
88 {
89 println!(
90 "Package source is a path in the repo: {}",
91 package_source.cyan()
92 );
93 let pkg_url = get_repo_file_url(&provider, &repo_path, package_source)?;
94 let client = crate::pkg::utils::get_http_client()?;
95 let pkg_content = client.get(&pkg_url).send()?.text()?;
96 let temp_path = env::temp_dir().join(format!(
97 "zoi-repo-install-{}.pkg.lua",
98 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
99 ));
100 fs::write(&temp_path, pkg_content)?;
101 temp_path
102 .to_str()
103 .ok_or_else(|| anyhow!("Temporary path contains invalid UTF-8"))?
104 .to_string()
105 } else {
106 println!(
107 "Package source is a package name: {}",
108 package_source.cyan()
109 );
110 package_source.to_string()
111 };
112
113 crate::cmd::install::run(
114 &[source_to_install],
115 None,
116 force,
117 all_optional,
118 yes,
119 scope_override.map(|s| match s {
120 types::Scope::User => crate::cli::InstallScope::User,
121 types::Scope::System => crate::cli::InstallScope::System,
122 types::Scope::Project => crate::cli::InstallScope::Project,
123 }),
124 false,
125 false,
126 false,
127 None,
128 false,
129 plugin_manager,
130 false,
131 false,
132 false,
133 false,
134 3,
135 false,
136 false,
137 None,
138 )?;
139
140 Ok(())
141}
142
143fn parse_repo_spec(spec: &str) -> Result<(String, String)> {
144 if let Some((provider_alias, path)) = spec.split_once(':') {
145 let provider = match provider_alias {
146 "gh" | "github" => "github",
147 "gl" | "gitlab" => "gitlab",
148 "cb" | "codeberg" => "codeberg",
149 _ => return Err(anyhow!("Unknown provider alias: {}", provider_alias)),
150 };
151 Ok((provider.to_string(), path.to_string()))
152 } else {
153 Ok(("github".to_string(), spec.to_string()))
154 }
155}
156
157fn get_repo_file_url(provider: &str, repo_path: &str, file_path: &str) -> Result<String> {
158 let branches = ["main", "master"];
159 let client = crate::pkg::utils::get_http_client()?;
160 for branch in &branches {
161 let url = match provider {
162 "github" => format!(
163 "https://raw.githubusercontent.com/{}/refs/heads/{}/{}",
164 repo_path, branch, file_path
165 ),
166 "gitlab" => format!(
167 "https://gitlab.com/{}/-/raw/{}/{}",
168 repo_path, branch, file_path
169 ),
170 "codeberg" => format!(
171 "https://codeberg.org/{}/raw/branch/{}/{}",
172 repo_path, branch, file_path
173 ),
174 _ => return Err(anyhow!("Unknown provider")),
175 };
176
177 let res = client.get(&url).send();
178 if let Ok(response) = res
179 && response.status().is_success()
180 {
181 return Ok(url);
182 }
183 }
184 Err(anyhow!(
185 "Could not find '{}' in repo '{}' on branches main or master.",
186 file_path,
187 repo_path
188 ))
189}