1use std::path::Path;
11
12use algocline_engine::card;
13
14use super::hub;
15use super::AppService;
16
17impl AppService {
18 pub fn card_list(&self, pkg: Option<&str>) -> Result<String, String> {
20 let rows = card::list(pkg)?;
21 Ok(card::summaries_to_json(&rows).to_string())
22 }
23
24 pub fn card_get(&self, card_id: &str) -> Result<String, String> {
26 match card::get(card_id)? {
27 Some(v) => Ok(v.to_string()),
28 None => Err(format!("card '{card_id}' not found")),
29 }
30 }
31
32 pub fn card_find(
34 &self,
35 pkg: Option<String>,
36 where_: Option<serde_json::Value>,
37 order_by: Option<serde_json::Value>,
38 limit: Option<usize>,
39 offset: Option<usize>,
40 ) -> Result<String, String> {
41 let where_parsed = match where_ {
42 Some(v) => Some(card::parse_where(&v)?),
43 None => None,
44 };
45 let order_parsed = match order_by {
46 Some(v) => card::parse_order_by(&v)?,
47 None => Vec::new(),
48 };
49 let q = card::FindQuery {
50 pkg,
51 where_: where_parsed,
52 order_by: order_parsed,
53 limit,
54 offset,
55 };
56 let rows = card::find(q)?;
57 Ok(card::summaries_to_json(&rows).to_string())
58 }
59
60 pub fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
62 match card::get_by_alias(name)? {
63 Some(v) => Ok(v.to_string()),
64 None => Err(format!("alias '{name}' not found")),
65 }
66 }
67
68 pub fn card_alias_list(&self, pkg: Option<&str>) -> Result<String, String> {
70 let rows = card::alias_list(pkg)?;
71 Ok(card::aliases_to_json(&rows).to_string())
72 }
73
74 pub fn card_alias_set(
76 &self,
77 name: &str,
78 card_id: &str,
79 pkg: Option<&str>,
80 note: Option<&str>,
81 ) -> Result<String, String> {
82 let alias = card::alias_set(name, card_id, pkg, note)?;
83 let arr = card::aliases_to_json(std::slice::from_ref(&alias));
84 let single = arr
85 .as_array()
86 .and_then(|a| a.first().cloned())
87 .unwrap_or(serde_json::Value::Null);
88 Ok(single.to_string())
89 }
90
91 pub fn card_append(&self, card_id: &str, fields: serde_json::Value) -> Result<String, String> {
93 let merged = card::append(card_id, fields)?;
94 Ok(merged.to_string())
95 }
96
97 pub async fn card_install(&self, url: String) -> Result<String, String> {
103 let local_path = Path::new(&url);
105 if local_path.is_absolute() && local_path.is_dir() {
106 return self.card_install_from_dir(local_path, &url);
107 }
108
109 let git_url = if url.starts_with("http://")
111 || url.starts_with("https://")
112 || url.starts_with("file://")
113 || url.starts_with("git@")
114 {
115 url.clone()
116 } else {
117 format!("https://{url}")
118 };
119
120 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
122
123 let output = tokio::process::Command::new("git")
124 .args([
125 "clone",
126 "--depth",
127 "1",
128 &git_url,
129 &staging.path().to_string_lossy(),
130 ])
131 .output()
132 .await
133 .map_err(|e| format!("Failed to run git: {e}"))?;
134
135 if !output.status.success() {
136 let stderr = String::from_utf8_lossy(&output.stderr);
137 return Err(format!("git clone failed: {stderr}"));
138 }
139
140 self.card_install_from_dir(staging.path(), &url)
141 }
142
143 fn card_install_from_dir(&self, root: &Path, source: &str) -> Result<String, String> {
145 let manifest_path = root.join("alc_cards.toml");
147 if !manifest_path.exists() {
148 return Err("Not a Card Collection: alc_cards.toml not found at root. \
149 Card Collections must have an alc_cards.toml manifest."
150 .into());
151 }
152
153 let mut all_imported: Vec<String> = Vec::new();
154 let mut all_skipped: Vec<String> = Vec::new();
155 let mut packages: Vec<String> = Vec::new();
156
157 let entries =
158 std::fs::read_dir(root).map_err(|e| format!("Failed to read source dir: {e}"))?;
159
160 for entry in entries.flatten() {
161 let path = entry.path();
162 if !path.is_dir() {
163 continue;
164 }
165 let pkg_name = match entry.file_name().to_str() {
166 Some(n) if !n.starts_with('_') && !n.starts_with('.') => n.to_string(),
167 _ => continue,
168 };
169
170 let has_toml = std::fs::read_dir(&path)
172 .map(|entries| {
173 entries
174 .flatten()
175 .any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
176 })
177 .unwrap_or(false);
178
179 if !has_toml {
180 continue;
181 }
182
183 let (imported, skipped) = card::import_from_dir(&path, &pkg_name)?;
184 if !imported.is_empty() || !skipped.is_empty() {
185 packages.push(pkg_name);
186 }
187 all_imported.extend(imported);
188 all_skipped.extend(skipped);
189 }
190
191 if all_imported.is_empty() && all_skipped.is_empty() {
192 return Err("No Card files found in any subdirectory.".into());
193 }
194
195 hub::register_source(source, "card_install");
197
198 let response = serde_json::json!({
199 "installed_cards": all_imported,
200 "skipped_cards": all_skipped,
201 "packages": packages,
202 "source": source,
203 "mode": "card_collection",
204 });
205 Ok(response.to_string())
206 }
207
208 pub(crate) fn import_pkg_bundled_cards(pkg_name: &str, cards_dir: &Path) -> Vec<String> {
213 match card::import_from_dir(cards_dir, pkg_name) {
214 Ok((imported, _)) => imported,
215 Err(e) => {
216 tracing::warn!("Failed to import bundled cards for '{pkg_name}': {e}");
217 Vec::new()
218 }
219 }
220 }
221
222 pub fn card_samples(
224 &self,
225 card_id: &str,
226 offset: usize,
227 limit: Option<usize>,
228 where_: Option<serde_json::Value>,
229 ) -> Result<String, String> {
230 let where_parsed = match where_ {
231 Some(v) => Some(card::parse_where(&v)?),
232 None => None,
233 };
234 let q = card::SamplesQuery {
235 offset,
236 limit,
237 where_: where_parsed,
238 };
239 let rows = card::read_samples(card_id, q)?;
240 Ok(serde_json::Value::Array(rows).to_string())
241 }
242
243 pub fn card_lineage(
245 &self,
246 card_id: &str,
247 direction: Option<&str>,
248 depth: Option<usize>,
249 include_stats: Option<bool>,
250 relation_filter: Option<Vec<String>>,
251 ) -> Result<String, String> {
252 let dir = match direction {
253 Some(s) => card::LineageDirection::parse(s)?,
254 None => card::LineageDirection::Up,
255 };
256 let q = card::LineageQuery {
257 card_id: card_id.to_string(),
258 direction: dir,
259 depth,
260 include_stats: include_stats.unwrap_or(true),
261 relation_filter,
262 };
263 match card::lineage(q)? {
264 Some(res) => Ok(card::lineage_to_json(&res).to_string()),
265 None => Err(format!("card '{card_id}' not found")),
266 }
267 }
268}