oxibrain_cli/cmd/
model.rs1use anyhow::Context as _;
4use oxibrain::models::{
5 cli_progress, default_manifest, digest_file, load_manifest, model_dir, pull_entry,
6 save_manifest, verify_entry,
7};
8
9pub async fn run(args: &crate::cli::ModelCmd) -> anyhow::Result<()> {
10 let dir = model_dir();
11 match args {
12 crate::cli::ModelCmd::List => list(&dir).await,
13 crate::cli::ModelCmd::Pull { name } => pull(&dir, name.as_deref()).await,
14 crate::cli::ModelCmd::Verify { name } => verify(&dir, name.as_deref()).await,
15 crate::cli::ModelCmd::Use { name } => r#use(&dir, name).await,
16 }
17}
18
19async fn list(dir: &std::path::Path) -> anyhow::Result<()> {
20 let manifest = load_manifest().context("load manifest")?;
21 println!("models dir: {}", dir.display());
22 if manifest.is_empty() {
23 println!("(no models installed — run `oxibrain model pull`)");
24 return Ok(());
25 }
26 println!(
27 "{:<16} {:<24} {:>8} {:>6} file",
28 "role", "name", "size", "status"
29 );
30 println!("{}", "-".repeat(70));
31 for entry in &manifest {
32 let path = dir.join(&entry.file);
33 let status = if !path.exists() {
34 "missing"
35 } else if verify_entry(entry, dir).is_ok() {
36 "ok"
37 } else {
38 "corrupt"
39 };
40 println!(
41 "{:<16} {:<24} {:>6} MiB {:>6} {}",
42 entry.role.as_str(),
43 entry.name,
44 entry.size_mb,
45 status,
46 entry.file
47 );
48 }
49 Ok(())
50}
51
52async fn pull(dir: &std::path::Path, name: Option<&str>) -> anyhow::Result<()> {
53 let defaults = default_manifest();
54 let mut manifest = load_manifest().context("load manifest")?;
55
56 for d in &defaults {
58 if !manifest.iter().any(|m| m.name == d.name) {
59 manifest.push(d.clone());
60 }
61 }
62
63 let targets: Vec<_> = match name {
64 Some(n) => manifest
65 .iter()
66 .filter(|m| m.name == n || m.file == n)
67 .cloned()
68 .collect(),
69 None => manifest.clone(),
70 };
71 if targets.is_empty() {
72 anyhow::bail!("no model named `{}` in the manifest", name.unwrap_or(""));
73 }
74
75 for entry in &targets {
76 let path = dir.join(&entry.file);
77 if path.exists() && verify_entry(entry, dir).is_ok() {
78 println!("{} already present and verified", entry.name);
79 continue;
80 }
81 println!("pulling {} ({} MiB)...", entry.name, entry.size_mb);
82 let entry_for_pull = entry.clone();
83 pull_entry(&entry_for_pull, dir, cli_progress)
84 .await
85 .with_context(|| format!("pull {}", entry.name))?;
86 println!(" {} verified", entry.name);
87 }
88 save_manifest(&manifest).context("save manifest")?;
89 Ok(())
90}
91
92async fn verify(dir: &std::path::Path, name: Option<&str>) -> anyhow::Result<()> {
93 let manifest = load_manifest().context("load manifest")?;
94 let targets: Vec<_> = match name {
95 Some(n) => manifest.iter().filter(|m| m.name == n).collect(),
96 None => manifest.iter().collect(),
97 };
98 if targets.is_empty() {
99 println!("(no models in manifest)");
100 return Ok(());
101 }
102 for &entry in &targets {
103 let path = dir.join(&entry.file);
104 if !path.exists() {
105 println!("{}: MISSING ({})", entry.name, path.display());
106 continue;
107 }
108 let actual = digest_file(&path)?;
109 let ok = actual == entry.digest;
110 println!(
111 "{}: {} (expected {}..., got {}...)",
112 entry.name,
113 if ok { "ok" } else { "CORRUPT" },
114 &entry.digest[..entry.digest.len().min(12)],
115 &actual[..actual.len().min(12)],
116 );
117 }
118 Ok(())
119}
120
121async fn r#use(dir: &std::path::Path, name: &str) -> anyhow::Result<()> {
122 let manifest = load_manifest().context("load manifest")?;
123 let entry = manifest
124 .iter()
125 .find(|m| m.name == name)
126 .with_context(|| format!("model `{name}` not in manifest"))?;
127 let path = dir.join(&entry.file);
128 if !path.exists() {
129 anyhow::bail!("model file not downloaded: run `oxibrain model pull {name}`");
130 }
131 verify_entry(entry, dir).context("verify")?;
132 println!("using {} ({})", entry.name, entry.file);
135 println!("model path: {}", path.display());
136 println!("model digest: {}", entry.digest);
137 Ok(())
138}