1use std::io::Write;
2use std::process::ExitCode;
3
4use crate::api::Output;
5use crate::{cache, config, mycelium, skill_admin, spore, synapse, visitor};
6
7use super::*;
8
9fn emit_cli_json(message: &str) {
10 let mut stdout = std::io::stdout();
11 let _ = stdout.write_all(message.as_bytes());
12 let _ = stdout.write_all(b"\n");
13}
14
15fn build_runtime() -> Result<tokio::runtime::Runtime, ExitCode> {
16 tokio::runtime::Runtime::new().map_err(|e| {
17 emit_cli_json(&agent_first_data::output_json(&cli_error_value(
18 &format!("Failed to create async runtime: {}", e),
19 "retry the command; if it repeats, check available system resources",
20 )));
21 ExitCode::FAILURE
22 })
23}
24
25fn emit_startup(out: &Output, cli: &Cli, log: &[String]) {
26 if log
27 .iter()
28 .any(|f| matches!(f.as_str(), "startup" | "all" | "*"))
29 {
30 let mut args = serde_json::to_value(&cli.command)
31 .unwrap_or_else(|_| serde_json::Value::Object(Default::default()));
32 if let Some(obj) = args.as_object_mut() {
33 obj.insert(
34 "output".to_string(),
35 serde_json::Value::String(cli.output.clone()),
36 );
37 if let Ok(log_value) = serde_json::to_value(log) {
38 obj.insert("log".to_string(), log_value);
39 }
40 }
41 out.startup(args);
42 }
43}
44
45pub fn execute(cli: Cli) -> ExitCode {
46 let output_format = agent_first_data::cli_parse_output(&cli.output).unwrap_or_else(|e| {
47 emit_cli_json(&agent_first_data::output_json(&cli_error_value(
48 &e,
49 "use --output json, --output yaml, or --output plain",
50 )));
51 std::process::exit(2);
52 });
53
54 let log = agent_first_data::cli_parse_log_filters(&cli.log);
55 let out = Output::new(output_format);
56 emit_startup(&out, &cli, &log);
57
58 let rt = match build_runtime() {
59 Ok(rt) => rt,
60 Err(code) => return code,
61 };
62
63 match cli.command {
64 Commands::Sense { uri, id } => {
65 rt.block_on(visitor::handle_sense(&out, &uri, id.as_deref()))
66 }
67
68 Commands::Taste {
69 uri,
70 verdict,
71 notes,
72 synapse,
73 synapse_token_secret,
74 domain,
75 } => rt.block_on(visitor::handle_taste(
76 &out,
77 &uri,
78 verdict,
79 notes.as_deref(),
80 synapse.as_deref(),
81 synapse_token_secret.as_deref(),
82 domain.as_deref(),
83 )),
84
85 Commands::Spawn {
86 uri,
87 directory,
88 vcs,
89 dist,
90 bond,
91 } => rt.block_on(visitor::handle_spawn(
92 &out,
93 &uri,
94 directory.as_deref(),
95 vcs.map(|v| v.as_str()),
96 dist.map(|d| d.as_str()),
97 bond,
98 )),
99
100 Commands::Grow {
101 dist,
102 synapse,
103 synapse_token_secret,
104 bond,
105 } => rt.block_on(visitor::handle_grow(
106 &out,
107 None,
108 dist.map(|d| d.as_str()),
109 bond,
110 synapse.as_deref(),
111 synapse_token_secret.as_deref(),
112 )),
113
114 Commands::Absorb {
115 uris,
116 discover,
117 synapse,
118 synapse_token_secret,
119 max_depth,
120 } => rt.block_on(visitor::handle_absorb(
121 &out,
122 uris,
123 discover,
124 synapse.as_deref(),
125 synapse_token_secret.as_deref(),
126 max_depth,
127 )),
128
129 Commands::Bond { clean, status } => {
130 rt.block_on(visitor::handle_bond_fetch(&out, clean, status))
131 }
132
133 Commands::Replicate {
134 uris,
135 refs,
136 domain,
137 site_path,
138 } => rt.block_on(spore::handle_replicate(
139 &out,
140 uris,
141 refs,
142 &domain,
143 site_path.as_deref(),
144 )),
145
146 Commands::Hatch {
147 id,
148 version,
149 name,
150 domain,
151 synopsis,
152 intent,
153 mutations,
154 license,
155 command,
156 } => match command {
157 Some(HatchCommands::Bond { command }) => match command {
158 HatchBondCommands::Set {
159 uri,
160 relation,
161 id,
162 reason,
163 with_entries,
164 } => spore::handle_bond_set(&out, &uri, relation, id, reason, with_entries),
165 HatchBondCommands::Remove { uri, relation } => {
166 spore::handle_bond_remove(&out, uri, relation)
167 }
168 HatchBondCommands::Clear => spore::handle_bond_clear(&out),
169 },
170 Some(HatchCommands::Tree { command }) => match command {
171 HatchTreeCommands::Set {
172 algorithm,
173 exclude_names,
174 follow_rules,
175 } => spore::handle_tree_set(&out, algorithm, exclude_names, follow_rules),
176 HatchTreeCommands::Show => spore::handle_tree_show(&out),
177 },
178 None => spore::handle_hatch(
179 &out,
180 spore::HatchArgs {
181 id,
182 version,
183 name,
184 domain,
185 synopsis,
186 intent,
187 mutations,
188 license,
189 },
190 ),
191 },
192
193 Commands::Release {
194 domain,
195 source,
196 site_path,
197 dist_git,
198 dist_ref,
199 archive,
200 dry_run,
201 } => spore::handle_release(
202 &out,
203 spore::ReleaseArgs {
204 domain: &domain,
205 source,
206 site_path: site_path.as_deref(),
207 dist_git,
208 dist_ref,
209 archive: &archive,
210 dry_run,
211 },
212 ),
213
214 Commands::Lineage {
215 uri,
216 direction,
217 synapse,
218 synapse_token_secret,
219 max_depth,
220 } => rt.block_on(visitor::handle_lineage(
221 &out,
222 &uri,
223 direction.map(|d| d.as_str()),
224 synapse.as_deref(),
225 synapse_token_secret.as_deref(),
226 max_depth,
227 )),
228
229 Commands::Search {
230 query,
231 synapse,
232 synapse_token_secret,
233 domain,
234 license,
235 bonds,
236 limit,
237 } => rt.block_on(visitor::handle_search(
238 &out,
239 &query,
240 synapse.as_deref(),
241 synapse_token_secret.as_deref(),
242 domain.as_deref(),
243 license.as_deref(),
244 bonds.as_deref(),
245 limit,
246 )),
247
248 Commands::Mycelium { action } => match action {
249 MyceliumAction::Root {
250 domain,
251 hub,
252 site_path,
253 name,
254 synopsis,
255 bio,
256 endpoints_base,
257 } => mycelium::handle_init(
258 &out,
259 mycelium::InitArgs {
260 domain: domain.as_deref(),
261 hub: hub.as_deref(),
262 site_path: site_path.as_deref(),
263 name: name.as_deref(),
264 synopsis: synopsis.as_deref(),
265 bio: bio.as_deref(),
266 endpoints_base: endpoints_base.as_deref(),
267 },
268 ),
269 MyceliumAction::Nutrient { command } => match command {
270 NutrientCommands::Add {
271 domain,
272 method_type,
273 with_entries,
274 site_path,
275 } => mycelium::handle_nutrient_add(
276 &out,
277 &domain,
278 &method_type,
279 with_entries,
280 site_path.as_deref(),
281 ),
282 NutrientCommands::Remove {
283 domain,
284 method_type,
285 site_path,
286 } => mycelium::handle_nutrient_remove(
287 &out,
288 &domain,
289 &method_type,
290 site_path.as_deref(),
291 ),
292 NutrientCommands::Clear { domain, site_path } => {
293 mycelium::handle_nutrient_clear(&out, &domain, site_path.as_deref())
294 }
295 },
296 MyceliumAction::Status {
297 domain,
298 site_path,
299 id,
300 } => mycelium::handle_status(
301 &out,
302 domain.as_deref(),
303 site_path.as_deref(),
304 id.as_deref(),
305 ),
306 MyceliumAction::Pulse {
307 synapse,
308 synapse_token_secret,
309 file,
310 } => rt.block_on(mycelium::handle_pulse(
311 &out,
312 synapse.as_deref(),
313 synapse_token_secret.as_deref(),
314 &file,
315 )),
316 MyceliumAction::Serve {
317 domain,
318 site_path,
319 port,
320 } => mycelium::handle_serve(&out, domain.as_deref(), site_path.as_deref(), port),
321 },
322
323 Commands::Synapse { action } => match action {
324 SynapseAction::Discover {
325 synapse,
326 synapse_token_secret,
327 } => rt.block_on(synapse::handle_discover(
328 &out,
329 synapse.as_deref(),
330 synapse_token_secret.as_deref(),
331 )),
332 SynapseAction::List => synapse::handle_list(&out),
333 SynapseAction::Health {
334 synapse,
335 synapse_token_secret,
336 } => rt.block_on(synapse::handle_info(
337 &out,
338 synapse.as_deref(),
339 synapse_token_secret.as_deref(),
340 )),
341 SynapseAction::Add { url } => synapse::handle_add(&out, &url),
342 SynapseAction::Remove { domain } => synapse::handle_remove(&out, &domain),
343 SynapseAction::Use { domain } => synapse::handle_use(&out, &domain),
344 SynapseAction::Config {
345 domain,
346 token_secret,
347 } => synapse::handle_config(&out, &domain, token_secret.as_deref()),
348 },
349
350 Commands::Cache { action } => match action {
351 CacheAction::List => cache::handle_list(&out),
352 CacheAction::Clean { all } => cache::handle_clean(&out, all),
353 CacheAction::Path { uri } => cache::handle_path(&out, &uri),
354 },
355
356 Commands::Config { action } => match action {
357 ConfigAction::List => config::handle_list(&out),
358 ConfigAction::Set { key, value } => config::handle_set(&out, &key, &value),
359 },
360
361 Commands::Skill { action } => skill_admin::handle_skill(&out, action),
362 }
363}