objectiveai_cli/filesystem/
publish.rs1use std::path::{Path, PathBuf};
2
3use super::{Client, Error};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum Kind {
8 Agents,
9 Swarms,
10 Functions,
11 Profiles,
12}
13
14impl Kind {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Kind::Agents => "agents",
18 Kind::Swarms => "swarms",
19 Kind::Functions => "functions",
20 Kind::Profiles => "profiles",
21 }
22 }
23
24 pub fn filename(self) -> &'static str {
26 match self {
27 Kind::Agents => "agent.json",
28 Kind::Swarms => "swarm.json",
29 Kind::Functions => "function.json",
30 Kind::Profiles => "profile.json",
31 }
32 }
33}
34
35fn validate_repository_name(name: &str) -> Result<(), Error> {
36 if name.is_empty() || name.len() > 100 {
37 return Err(Error::InvalidRepositoryName(name.to_string()));
38 }
39 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
40 return Err(Error::InvalidRepositoryName(name.to_string()));
41 }
42 Ok(())
43}
44
45fn repo_path(client: &Client, kind: Kind, repository: &str) -> PathBuf {
46 client
47 .state_dir()
48 .join(kind.as_str())
49 .join(&client.commit_author_name)
50 .join(repository)
51}
52
53pub async fn publish_agent(
55 client: &Client,
56 repository: &str,
57 agent: &objectiveai_sdk::agent::RemoteAgentBaseWithFallbacks,
58 message: &str,
59 overwrite: bool,
60) -> Result<String, Error> {
61 let content =
62 serde_json::to_string_pretty(agent).map_err(Error::Serialize)?;
63 publish(
64 client,
65 Kind::Agents,
66 repository,
67 &content,
68 message,
69 overwrite,
70 )
71 .await
72}
73
74pub async fn publish_swarm(
76 client: &Client,
77 repository: &str,
78 swarm: &objectiveai_sdk::swarm::RemoteSwarmBase,
79 message: &str,
80 overwrite: bool,
81) -> Result<String, Error> {
82 let content =
83 serde_json::to_string_pretty(swarm).map_err(Error::Serialize)?;
84 publish(
85 client,
86 Kind::Swarms,
87 repository,
88 &content,
89 message,
90 overwrite,
91 )
92 .await
93}
94
95pub async fn publish_function(
97 client: &Client,
98 repository: &str,
99 function: &objectiveai_sdk::functions::FullRemoteFunction,
100 message: &str,
101 overwrite: bool,
102) -> Result<String, Error> {
103 let content =
104 serde_json::to_string_pretty(function).map_err(Error::Serialize)?;
105 publish(
106 client,
107 Kind::Functions,
108 repository,
109 &content,
110 message,
111 overwrite,
112 )
113 .await
114}
115
116pub async fn publish_profile(
118 client: &Client,
119 repository: &str,
120 profile: &objectiveai_sdk::functions::RemoteProfile,
121 message: &str,
122 overwrite: bool,
123) -> Result<String, Error> {
124 let content =
125 serde_json::to_string_pretty(profile).map_err(Error::Serialize)?;
126 publish(
127 client,
128 Kind::Profiles,
129 repository,
130 &content,
131 message,
132 overwrite,
133 )
134 .await
135}
136
137async fn publish(
142 client: &Client,
143 kind: Kind,
144 repository: &str,
145 content: &str,
146 message: &str,
147 overwrite: bool,
148) -> Result<String, Error> {
149 validate_repository_name(repository)?;
150
151 let path = repo_path(client, kind, repository);
152 let filename = kind.filename();
153
154 tokio::fs::create_dir_all(&path).await?;
155
156 if !overwrite && tokio::fs::metadata(path.join(filename)).await.is_ok() {
158 return Err(Error::Write(
159 path.join(filename),
160 std::io::Error::new(
161 std::io::ErrorKind::AlreadyExists,
162 "file already exists (use overwrite to replace)",
163 ),
164 ));
165 }
166
167 let commit_author_name = client.commit_author_name.clone();
173 let commit_author_email = client.commit_author_email.clone();
174 let message = message.to_string();
175 let filename = filename.to_string();
176 let content = content.to_string();
177
178 tokio::task::spawn_blocking(move || {
179 git_commit(
180 &path,
181 &filename,
182 &content,
183 &commit_author_name,
184 &commit_author_email,
185 &message,
186 )
187 })
188 .await
189 .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?
190}
191
192fn git_commit(
201 repo_path: &Path,
202 filename: &str,
203 content: &str,
204 author_name: &str,
205 author_email: &str,
206 message: &str,
207) -> Result<String, Error> {
208 let repo = match git2::Repository::open(repo_path) {
210 Ok(repo) => {
211 let mut checkout = git2::build::CheckoutBuilder::new();
213 checkout.force();
214 checkout.remove_untracked(true);
215 if let Ok(head) = repo.head() {
216 if let Ok(commit) = head.peel_to_commit() {
217 repo.reset(
218 commit.as_object(),
219 git2::ResetType::Hard,
220 Some(&mut checkout),
221 )?;
222 }
223 }
224 repo
225 }
226 Err(_) => git2::Repository::init(repo_path)?,
227 };
228
229 let file_path = repo_path.join(filename);
232 std::fs::write(&file_path, content.as_bytes())
233 .map_err(|e| Error::Write(file_path, e))?;
234
235 let mut index = repo.index()?;
237 index.add_path(Path::new(filename))?;
238 index.write()?;
239 let tree_oid = index.write_tree()?;
240
241 let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
243 if let Some(ref parent) = parent {
244 if parent.tree_id() == tree_oid {
245 return Ok(parent.id().to_string());
246 }
247 }
248
249 let tree = repo.find_tree(tree_oid)?;
250
251 let sig = git2::Signature::now(author_name, author_email)?;
253 let parents: Vec<&git2::Commit> = parent.iter().collect();
254 let commit_oid =
255 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)?;
256
257 Ok(commit_oid.to_string())
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn read(dir: &Path, name: &str) -> String {
265 std::fs::read_to_string(dir.join(name)).unwrap()
266 }
267
268 #[test]
274 fn overwrite_changes_content_and_sha() {
275 let dir = tempfile::tempdir().unwrap();
276 let p = dir.path();
277 let (name, email) = ("alice", "alice@example.com");
278
279 let sha1 = git_commit(p, "agent.json", "v1", name, email, "first").unwrap();
280 assert_eq!(read(p, "agent.json"), "v1");
281
282 let sha2 = git_commit(p, "agent.json", "v2", name, email, "second").unwrap();
283 assert_eq!(read(p, "agent.json"), "v2", "overwrite must update the file on disk");
284 assert_ne!(sha1, sha2, "overwrite with new content must create a new commit");
285 }
286
287 #[test]
290 fn identical_content_is_a_noop() {
291 let dir = tempfile::tempdir().unwrap();
292 let p = dir.path();
293 let (name, email) = ("bob", "bob@example.com");
294
295 let sha1 = git_commit(p, "function.json", "same", name, email, "first").unwrap();
296 let sha2 = git_commit(p, "function.json", "same", name, email, "again").unwrap();
297 assert_eq!(sha1, sha2, "identical content must not create a new commit");
298 }
299}