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