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 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 tokio::fs::write(path.join(filename), content.as_bytes()).await?;
169
170 let commit_author_name = client.commit_author_name.clone();
172 let commit_author_email = client.commit_author_email.clone();
173 let message = message.to_string();
174 let filename = filename.to_string();
175
176 tokio::task::spawn_blocking(move || {
177 git_commit(
178 &path,
179 &filename,
180 &commit_author_name,
181 &commit_author_email,
182 &message,
183 )
184 })
185 .await
186 .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?
187}
188
189fn git_commit(
191 repo_path: &Path,
192 filename: &str,
193 author_name: &str,
194 author_email: &str,
195 message: &str,
196) -> Result<String, Error> {
197 let repo = match git2::Repository::open(repo_path) {
199 Ok(repo) => {
200 let mut checkout = git2::build::CheckoutBuilder::new();
202 checkout.force();
203 checkout.remove_untracked(true);
204 if let Ok(head) = repo.head() {
205 if let Ok(commit) = head.peel_to_commit() {
206 repo.reset(
207 commit.as_object(),
208 git2::ResetType::Hard,
209 Some(&mut checkout),
210 )?;
211 }
212 }
213 repo
214 }
215 Err(_) => git2::Repository::init(repo_path)?,
216 };
217
218 let mut index = repo.index()?;
220 index.add_path(Path::new(filename))?;
221 index.write()?;
222 let tree_oid = index.write_tree()?;
223
224 let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
226 if let Some(ref parent) = parent {
227 if parent.tree_id() == tree_oid {
228 return Ok(parent.id().to_string());
229 }
230 }
231
232 let tree = repo.find_tree(tree_oid)?;
233
234 let sig = git2::Signature::now(author_name, author_email)?;
236 let parents: Vec<&git2::Commit> = parent.iter().collect();
237 let commit_oid =
238 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)?;
239
240 Ok(commit_oid.to_string())
241}