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