apr_cli/commands/
pull_list.rs1
2fn scan_cache_dir_for_orphans(
8 cache_dir: &Path,
9 known_paths: &HashSet<std::path::PathBuf>,
10) -> Vec<DiskModelEntry> {
11 let Ok(read_dir) = std::fs::read_dir(cache_dir) else {
12 return Vec::new();
13 };
14 let mut orphans = Vec::new();
15 for entry in read_dir.flatten() {
16 let path = entry.path();
17 if !path.is_file() {
18 continue;
19 }
20 if known_paths.contains(&path) {
21 continue;
22 }
23 let Some(ext) = path.extension().and_then(|s| s.to_str()) else {
24 continue;
25 };
26 let format = match ext {
27 "gguf" | "ggml" => "GGUF",
28 "apr" => "APR",
29 "safetensors" => "SafeTensors",
30 _ => continue,
31 };
32 let size_bytes = entry.metadata().map(|m| m.len()).unwrap_or(0);
33 let name = path
34 .file_stem()
35 .and_then(|s| s.to_str())
36 .unwrap_or("unknown")
37 .to_string();
38 orphans.push(DiskModelEntry {
39 name,
40 size_bytes,
41 format,
42 path: path.clone(),
43 });
44 }
45 orphans
46}
47
48struct DiskModelEntry {
49 name: String,
50 size_bytes: u64,
51 format: &'static str,
52 path: std::path::PathBuf,
53}
54
55#[allow(clippy::disallowed_methods)]
61pub fn list(json: bool, quiet: bool) -> Result<()> {
62 let fetcher = ModelFetcher::new().map_err(|e| {
63 CliError::ValidationFailed(format!("Failed to initialize model fetcher: {e}"))
64 })?;
65
66 let models = fetcher.list();
67
68 let known_paths: HashSet<std::path::PathBuf> =
74 models.iter().map(|m| m.path.clone()).collect();
75 let orphans = scan_cache_dir_for_orphans(fetcher.cache_dir(), &known_paths);
76
77 if quiet {
80 for m in &models {
81 println!("{}", m.name);
82 }
83 for o in &orphans {
84 println!("{}", o.name);
85 }
86 return Ok(());
87 }
88
89 if json {
91 let mut models_json: Vec<serde_json::Value> = models
92 .iter()
93 .map(|m| {
94 serde_json::json!({
95 "name": m.name,
96 "size_bytes": m.size_bytes,
97 "format": m.format.name(),
98 "path": m.path.display().to_string(),
99 "source": "manifest",
100 })
101 })
102 .collect();
103 for o in &orphans {
105 models_json.push(serde_json::json!({
106 "name": o.name,
107 "size_bytes": o.size_bytes,
108 "format": o.format,
109 "path": o.path.display().to_string(),
110 "source": "disk_scan",
111 }));
112 }
113 let stats = fetcher.stats();
114 let orphan_bytes: u64 = orphans.iter().map(|o| o.size_bytes).sum();
115 let output = serde_json::json!({
116 "models": models_json,
117 "total": models.len() + orphans.len(),
118 "total_size_bytes": stats.total_size_bytes + orphan_bytes,
119 });
120 println!(
121 "{}",
122 serde_json::to_string_pretty(&output).unwrap_or_default()
123 );
124 return Ok(());
125 }
126
127 println!("{}", "=== Cached Models ===".cyan().bold());
128 println!();
129
130 if models.is_empty() && orphans.is_empty() {
131 println!("{}", "No cached models found.".dimmed());
132 println!();
133 println!("Pull a model with:");
134 println!(" apr pull hf://Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf");
135 println!();
136 println!("Or run directly (auto-downloads):");
137 println!(" apr run hf://Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf");
138 return Ok(());
139 }
140
141 println!(
143 "{:<40} {:<12} {:<12} {}",
144 "NAME".dimmed(),
145 "SIZE".dimmed(),
146 "FORMAT".dimmed(),
147 "PATH".dimmed()
148 );
149 println!("{}", "-".repeat(104).dimmed());
150
151 for model in &models {
152 let size = format_bytes(model.size_bytes);
153 let format = model.format.name();
154 let name = if model.name.len() > 38 {
155 format!("{}...", &model.name[..35])
156 } else {
157 model.name.clone()
158 };
159
160 println!(
161 "{:<40} {:<12} {:<12} {}",
162 name.cyan(),
163 size.yellow(),
164 format,
165 model.path.display().to_string().dimmed()
166 );
167 }
168
169 for o in &orphans {
171 let size = format_bytes(o.size_bytes);
172 let name = if o.name.len() > 38 {
173 format!("{}...", &o.name[..35])
174 } else {
175 o.name.clone()
176 };
177 println!(
178 "{:<40} {:<12} {:<12} {} {}",
179 name.cyan(),
180 size.yellow(),
181 o.format,
182 o.path.display().to_string().dimmed(),
183 "(orphan)".dimmed()
184 );
185 }
186
187 println!();
188
189 let stats = fetcher.stats();
191 let orphan_bytes: u64 = orphans.iter().map(|o| o.size_bytes).sum();
192 let total_count = models.len() + orphans.len();
193 let total_bytes = stats.total_size_bytes + orphan_bytes;
194 if orphans.is_empty() {
195 println!("Total: {} models, {} used", total_count, format_bytes(total_bytes));
196 } else {
197 println!(
198 "Total: {} models ({} tracked + {} orphans), {} used",
199 total_count,
200 models.len(),
201 orphans.len(),
202 format_bytes(total_bytes)
203 );
204 }
205
206 Ok(())
207}