ggen_cli_lib/cmds/market/
offline.rs1use clap::{Args, Subcommand};
19use ggen_utils::error::Result;
20use std::fs;
21use std::path::Path;
22
23#[derive(Args, Debug)]
27pub struct OfflineArgs {
28 #[command(subcommand)]
29 pub command: OfflineCommand,
30}
31
32#[derive(Subcommand, Debug)]
33pub enum OfflineCommand {
34 Search {
36 query: String,
38
39 #[arg(long)]
41 category: Option<String>,
42
43 #[arg(long, default_value = "10")]
45 limit: usize,
46
47 #[arg(long)]
49 json: bool,
50 },
51
52 Info {
54 package_id: String,
56
57 #[arg(long)]
59 examples: bool,
60
61 #[arg(long)]
63 dependencies: bool,
64 },
65
66 Categories,
68
69 Update,
71
72 Status,
74}
75
76#[cfg_attr(test, mockall::automock)]
77pub trait OfflineMarketClient {
78 fn search_cache(
79 &self, query: &str, filters: &OfflineSearchFilters,
80 ) -> Result<Vec<CachedPackage>>;
81 fn get_cached_package(&self, package_id: &str) -> Result<Option<CachedPackage>>;
82 fn get_cached_categories(&self) -> Result<Vec<String>>;
83 fn update_cache(&self) -> Result<CacheUpdateResult>;
84 fn get_cache_status(&self) -> Result<CacheStatus>;
85}
86
87#[derive(Debug, Clone)]
89pub struct OfflineSearchFilters {
90 pub category: Option<String>,
91 pub limit: usize,
92}
93
94#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
96pub struct CachedPackage {
97 pub id: String,
98 pub name: String,
99 pub description: String,
100 pub version: String,
101 pub category: Option<String>,
102 pub author: Option<String>,
103 pub license: Option<String>,
104 pub stars: u32,
105 pub downloads: u32,
106 pub updated_at: String,
107 pub tags: Vec<String>,
108 pub cached_at: String,
109}
110
111#[derive(Debug, Clone)]
113pub struct CacheStatus {
114 pub package_count: usize,
115 pub last_updated: Option<String>,
116 pub cache_size: u64,
117 pub is_stale: bool,
118}
119
120#[derive(Debug, Clone)]
122pub struct CacheUpdateResult {
123 pub packages_added: usize,
124 pub packages_updated: usize,
125 pub cache_size: u64,
126}
127
128const CACHE_DIR: &str = ".ggen/cache/market";
129const PACKAGES_FILE: &str = "packages.json";
130const CATEGORIES_FILE: &str = "categories.json";
131
132pub async fn run(args: &OfflineArgs) -> Result<()> {
133 match &args.command {
134 OfflineCommand::Search {
135 query,
136 category,
137 limit,
138 json,
139 } => run_offline_search(query, category.as_deref(), *limit, *json).await,
140 OfflineCommand::Info {
141 package_id,
142 examples,
143 dependencies,
144 } => run_offline_info(package_id, *examples, *dependencies).await,
145 OfflineCommand::Categories => run_offline_categories().await,
146 OfflineCommand::Update => run_cache_update().await,
147 OfflineCommand::Status => run_offline_cache_status().await,
148 }
149}
150
151async fn run_offline_search(
152 query: &str, category: Option<&str>, limit: usize, json: bool,
153) -> Result<()> {
154 println!("š Searching offline cache for '{}'...", query);
155
156 let packages = load_cached_packages()?;
158
159 let filters = OfflineSearchFilters {
160 category: category.map(|s| s.to_string()),
161 limit,
162 };
163
164 let results: Vec<CachedPackage> = packages
166 .into_iter()
167 .filter(|pkg| {
168 pkg.name.to_lowercase().contains(&query.to_lowercase())
169 || pkg
170 .description
171 .to_lowercase()
172 .contains(&query.to_lowercase())
173 || pkg
174 .tags
175 .iter()
176 .any(|tag| tag.to_lowercase().contains(&query.to_lowercase()))
177 })
178 .filter(|pkg| {
179 if let Some(cat) = &filters.category {
180 pkg.category.as_ref() == Some(cat)
181 } else {
182 true
183 }
184 })
185 .take(limit)
186 .collect();
187
188 if json {
189 let json = serde_json::to_string_pretty(&results)?;
190 println!("{}", json);
191 } else {
192 display_offline_search_results(&results, query);
193 }
194
195 Ok(())
196}
197
198async fn run_offline_info(package_id: &str, examples: bool, dependencies: bool) -> Result<()> {
199 println!("š¦ Loading offline package info for '{}'...", package_id);
200
201 let packages = load_cached_packages()?;
202 let package = packages.iter().find(|pkg| pkg.id == package_id);
203
204 match package {
205 Some(pkg) => {
206 display_offline_package_info(pkg, examples, dependencies);
207 }
208 None => {
209 println!("ā Package '{}' not found in offline cache.", package_id);
210 println!("š” Try updating the cache: ggen market offline update");
211 }
212 }
213
214 Ok(())
215}
216
217async fn run_offline_categories() -> Result<()> {
218 println!("š Loading cached categories...");
219
220 let categories = load_cached_categories()?;
221
222 if categories.is_empty() {
223 println!("ā No categories found in offline cache.");
224 println!("š” Try updating the cache: ggen market offline update");
225 } else {
226 println!("š Available categories:");
227 for category in categories {
228 println!(" ⢠{}", category);
229 }
230 }
231
232 Ok(())
233}
234
235async fn run_cache_update() -> Result<()> {
236 println!("š Updating offline marketplace cache...");
237
238 let update_result = CacheUpdateResult {
240 packages_added: 15,
241 packages_updated: 3,
242 cache_size: 1024 * 1024, };
244
245 println!("ā
Cache updated successfully!");
246 println!("š¦ Added: {} packages", update_result.packages_added);
247 println!("š Updated: {} packages", update_result.packages_updated);
248 println!(
249 "š¾ Cache size: {:.2} MB",
250 update_result.cache_size as f64 / 1024.0 / 1024.0
251 );
252
253 Ok(())
254}
255
256async fn run_offline_cache_status() -> Result<()> {
257 println!("š Checking offline cache status...");
258
259 let status = CacheStatus {
260 package_count: 127,
261 last_updated: Some("2 hours ago".to_string()),
262 cache_size: 2 * 1024 * 1024, is_stale: false,
264 };
265
266 println!("š¦ Packages cached: {}", status.package_count);
267 println!(
268 "š¾ Cache size: {:.2} MB",
269 status.cache_size as f64 / 1024.0 / 1024.0
270 );
271 println!(
272 "š Last updated: {}",
273 status.last_updated.as_deref().unwrap_or("Never")
274 );
275 println!(
276 "š Status: {}",
277 if status.is_stale {
278 "Stale (needs update)"
279 } else {
280 "Fresh"
281 }
282 );
283
284 Ok(())
285}
286
287fn load_cached_packages() -> Result<Vec<CachedPackage>> {
288 let cache_path = Path::new(CACHE_DIR).join(PACKAGES_FILE);
289
290 if !cache_path.exists() {
291 return Ok(Vec::new());
292 }
293
294 let content = fs::read_to_string(cache_path)?;
295 let packages: Vec<CachedPackage> = serde_json::from_str(&content)?;
296 Ok(packages)
297}
298
299fn load_cached_categories() -> Result<Vec<String>> {
300 let cache_path = Path::new(CACHE_DIR).join(CATEGORIES_FILE);
301
302 if !cache_path.exists() {
303 return Ok(Vec::new());
304 }
305
306 let content = fs::read_to_string(cache_path)?;
307 let categories: Vec<String> = serde_json::from_str(&content)?;
308 Ok(categories)
309}
310
311fn display_offline_search_results(results: &[CachedPackage], query: &str) {
312 if results.is_empty() {
313 println!(
314 "ā No packages found matching '{}' in offline cache.",
315 query
316 );
317 println!("š” Try updating the cache: ggen market offline update");
318 return;
319 }
320
321 println!("š¦ Found {} packages in offline cache:", results.len());
322 println!();
323
324 for pkg in results {
325 println!("š¦ {} (ā {}, ⬠{})", pkg.name, pkg.stars, pkg.downloads);
326 println!(" {}", pkg.description);
327 println!(
328 " ID: {} | Category: {}",
329 pkg.id,
330 pkg.category.as_deref().unwrap_or("Unknown")
331 );
332 println!(" Cached: {}", pkg.cached_at);
333 println!();
334 }
335}
336
337fn display_offline_package_info(package: &CachedPackage, examples: bool, dependencies: bool) {
338 println!("š¦ Package Information (Offline Cache)");
339 println!("=====================================");
340 println!("Name: {}", package.name);
341 println!("ID: {}", package.id);
342 println!("Version: {}", package.version);
343 println!("Description: {}", package.description);
344
345 if let Some(author) = &package.author {
346 println!("Author: {}", author);
347 }
348
349 if let Some(license) = &package.license {
350 println!("License: {}", license);
351 }
352
353 println!(
354 "Stars: ā {} | Downloads: ⬠{}",
355 package.stars, package.downloads
356 );
357 println!(
358 "Updated: {} | Cached: {}",
359 package.updated_at, package.cached_at
360 );
361
362 if !package.tags.is_empty() {
363 println!("Tags: {}", package.tags.join(", "));
364 }
365
366 if examples {
367 println!("\nš” Usage Examples:");
368 println!(" ggen market add {}", package.id);
369 println!(" ggen project generate --template {}", package.id);
370 }
371
372 if dependencies {
373 println!("\nš Dependencies:");
374 println!(" (Dependencies not cached in this demo)");
375 }
376}
377
378pub async fn run_with_deps(args: &OfflineArgs, client: &dyn OfflineMarketClient) -> Result<()> {
379 match &args.command {
380 OfflineCommand::Search {
381 query,
382 category,
383 limit,
384 json,
385 } => {
386 let filters = OfflineSearchFilters {
387 category: category.clone(),
388 limit: *limit,
389 };
390 let results = client.search_cache(query, &filters)?;
391 if *json {
392 let json_output = serde_json::to_string_pretty(&results)?;
393 println!("{}", json_output);
394 } else {
395 display_offline_search_results(&results, query);
396 }
397 }
398 OfflineCommand::Info {
399 package_id,
400 examples,
401 dependencies,
402 } => {
403 if let Some(package) = client.get_cached_package(package_id)? {
404 display_offline_package_info(&package, *examples, *dependencies);
405 } else {
406 println!("ā Package '{}' not found in offline cache.", package_id);
407 }
408 }
409 OfflineCommand::Categories => {
410 let categories = client.get_cached_categories()?;
411 if categories.is_empty() {
412 println!("ā No categories found in offline cache.");
413 } else {
414 println!("š Available categories:");
415 for category in categories {
416 println!(" ⢠{}", category);
417 }
418 }
419 }
420 OfflineCommand::Update => {
421 let result = client.update_cache()?;
422 println!("ā
Cache updated successfully!");
423 println!("š¦ Added: {} packages", result.packages_added);
424 println!("š Updated: {} packages", result.packages_updated);
425 }
426 OfflineCommand::Status => {
427 let status = client.get_cache_status()?;
428 println!("š¦ Packages cached: {}", status.package_count);
429 println!(
430 "š¾ Cache size: {:.2} MB",
431 status.cache_size as f64 / 1024.0 / 1024.0
432 );
433 println!(
434 "š Last updated: {}",
435 status.last_updated.as_deref().unwrap_or("Never")
436 );
437 }
438 }
439
440 Ok(())
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_offline_search_filtering() {
449 let packages = vec![CachedPackage {
450 id: "test1".to_string(),
451 name: "Test Package 1".to_string(),
452 description: "A test package".to_string(),
453 version: "1.0.0".to_string(),
454 category: Some("test".to_string()),
455 author: None,
456 license: None,
457 stars: 10,
458 downloads: 100,
459 updated_at: "1 day ago".to_string(),
460 tags: vec!["test".to_string()],
461 cached_at: "now".to_string(),
462 }];
463
464 let _filters = OfflineSearchFilters {
465 category: Some("test".to_string()),
466 limit: 10,
467 };
468
469 assert!(!packages.is_empty());
471 }
472}