objectiveai_cli/filesystem/tools/
client.rs1use std::path::{Path, PathBuf};
11
12use super::super::Client;
13use super::{Exec, Manifest, ManifestWithNameAndSource};
14
15async fn parse_manifest_file(path: &Path) -> Option<ManifestWithNameAndSource> {
21 let bytes = tokio::fs::read(path).await.ok()?;
22 let manifest: Manifest = serde_json::from_slice(&bytes).ok()?;
23 let name = path.parent()?.parent()?.file_name()?.to_str()?.to_string();
26 let source = path.to_string_lossy().into_owned();
27 Some(ManifestWithNameAndSource {
28 name,
29 manifest,
30 source,
31 })
32}
33
34pub(crate) fn platform_exec(exec: &Exec) -> Vec<String> {
37 if cfg!(target_os = "windows") {
38 exec.windows.clone()
39 } else if cfg!(target_os = "macos") {
40 exec.macos.clone()
41 } else {
42 exec.linux.clone()
43 }
44}
45
46impl Client {
47 pub fn tools_dir(&self) -> PathBuf {
49 self.bin_dir().join("tools")
50 }
51
52 pub fn tool_dir(&self, owner: &str, name: &str, version: &str) -> PathBuf {
55 self.tools_dir().join(owner).join(name).join(version)
56 }
57
58 pub async fn resolve_tool(
64 &self,
65 owner: &str,
66 name: &str,
67 version: &str,
68 ) -> Option<(Vec<String>, PathBuf)> {
69 let bundle = self.get_tool(owner, name, version).await?;
70 let dir = self.tool_dir(owner, name, version);
71 Some((platform_exec(&bundle.manifest.exec), dir))
72 }
73
74 pub async fn get_tool(
78 &self,
79 owner: &str,
80 name: &str,
81 version: &str,
82 ) -> Option<ManifestWithNameAndSource> {
83 let path = self.tool_dir(owner, name, version).join("objectiveai.json");
84 parse_manifest_file(&path).await
85 }
86
87 pub async fn list_tools(
96 &self,
97 offset: usize,
98 limit: usize,
99 ) -> Vec<ManifestWithNameAndSource> {
100 let paths = collect_manifest_paths(self.tools_dir()).await;
101 let futures = paths.into_iter().map(|p| async move {
102 let bundle = parse_manifest_file(&p).await?;
103 let modified = tokio::fs::metadata(&p)
104 .await
105 .ok()?
106 .modified()
107 .ok()?
108 .duration_since(std::time::SystemTime::UNIX_EPOCH)
109 .ok()?
110 .as_secs();
111 Some((modified, bundle))
112 });
113 let mut entries: Vec<(u64, ManifestWithNameAndSource)> =
114 futures::future::join_all(futures)
115 .await
116 .into_iter()
117 .flatten()
118 .collect();
119 entries.sort_by(|a, b| b.0.cmp(&a.0));
120 let iter = entries.into_iter().map(|(_, m)| m);
121 if offset > 0 || limit < usize::MAX {
122 iter.skip(offset).take(limit).collect()
123 } else {
124 iter.collect()
125 }
126 }
127}
128
129pub(crate) async fn collect_manifest_paths(root: PathBuf) -> Vec<PathBuf> {
133 let mut out: Vec<PathBuf> = Vec::new();
134 let Ok(mut owners) = tokio::fs::read_dir(&root).await else {
135 return out;
136 };
137 while let Ok(Some(owner_e)) = owners.next_entry().await {
138 let Ok(mut names) = tokio::fs::read_dir(owner_e.path()).await else {
139 continue;
140 };
141 while let Ok(Some(name_e)) = names.next_entry().await {
142 let Ok(mut versions) = tokio::fs::read_dir(name_e.path()).await else {
143 continue;
144 };
145 while let Ok(Some(ver_e)) = versions.next_entry().await {
146 let manifest = ver_e.path().join("objectiveai.json");
147 if tokio::fs::metadata(&manifest)
148 .await
149 .map(|m| m.is_file())
150 .unwrap_or(false)
151 {
152 out.push(manifest);
153 }
154 }
155 }
156 }
157 out
158}