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 sites,
204 check,
205 } => {
206 spore::handle_bond_sync(&out, relation, &spec, domain, site_path, &sites, check)
207 }
208 },
209 Some(HatchCommands::Tree { command }) => match command {
210 HatchTreeCommands::Set {
211 algorithm,
212 exclude_names,
213 follow_rules,
214 } => spore::handle_tree_set(&out, algorithm, exclude_names, follow_rules),
215 HatchTreeCommands::Show => spore::handle_tree_show(&out),
216 },
217 None => spore::handle_hatch(
218 &out,
219 spore::HatchArgs {
220 id,
221 version,
222 name,
223 domain,
224 synopsis,
225 intent,
226 mutations,
227 license,
228 },
229 ),
230 },
231
232 Commands::Release {
233 domain,
234 source,
235 site_path,
236 dist_git,
237 dist_ref,
238 archive,
239 dry_run,
240 } => spore::handle_release(
241 &out,
242 spore::ReleaseArgs {
243 domain: &domain,
244 source,
245 site_path: site_path.as_deref(),
246 dist_git,
247 dist_ref,
248 archive: &archive,
249 dry_run,
250 },
251 ),
252
253 Commands::Lineage {
254 uri,
255 direction,
256 synapse,
257 synapse_token_secret,
258 max_depth,
259 } => rt.block_on(visitor::handle_lineage(
260 &out,
261 &uri,
262 direction.map(|d| d.as_str()),
263 synapse.as_deref(),
264 synapse_token_secret.as_deref(),
265 max_depth,
266 )),
267
268 Commands::Search {
269 query,
270 synapse,
271 synapse_token_secret,
272 domain,
273 license,
274 bonds,
275 limit,
276 } => rt.block_on(visitor::handle_search(
277 &out,
278 &query,
279 synapse.as_deref(),
280 synapse_token_secret.as_deref(),
281 domain.as_deref(),
282 license.as_deref(),
283 bonds.as_deref(),
284 limit,
285 )),
286
287 Commands::Mycelium { action } => match action {
288 MyceliumAction::Root {
289 domain,
290 hub,
291 site_path,
292 name,
293 synopsis,
294 bio,
295 endpoints_base,
296 } => mycelium::handle_init(
297 &out,
298 mycelium::InitArgs {
299 domain: domain.as_deref(),
300 hub: hub.as_deref(),
301 site_path: site_path.as_deref(),
302 name: name.as_deref(),
303 synopsis: synopsis.as_deref(),
304 bio: bio.as_deref(),
305 endpoints_base: endpoints_base.as_deref(),
306 },
307 ),
308 MyceliumAction::Nutrient { command } => match command {
309 NutrientCommands::Add {
310 domain,
311 method_type,
312 with_entries,
313 site_path,
314 } => mycelium::handle_nutrient_add(
315 &out,
316 &domain,
317 &method_type,
318 with_entries,
319 site_path.as_deref(),
320 ),
321 NutrientCommands::Remove {
322 domain,
323 method_type,
324 site_path,
325 } => mycelium::handle_nutrient_remove(
326 &out,
327 &domain,
328 &method_type,
329 site_path.as_deref(),
330 ),
331 NutrientCommands::Clear { domain, site_path } => {
332 mycelium::handle_nutrient_clear(&out, &domain, site_path.as_deref())
333 }
334 },
335 MyceliumAction::Spore { command } => match command {
336 SporeCommands::Yank {
337 id,
338 domain,
339 site_path,
340 purge,
341 } => mycelium::handle_spore_yank(
342 &out,
343 domain.as_deref(),
344 &id,
345 site_path.as_deref(),
346 purge,
347 ),
348 SporeCommands::Unyank {
349 id,
350 domain,
351 site_path,
352 } => mycelium::handle_spore_unyank(
353 &out,
354 domain.as_deref(),
355 &id,
356 site_path.as_deref(),
357 ),
358 },
359 MyceliumAction::Status {
360 domain,
361 site_path,
362 id,
363 } => mycelium::handle_status(
364 &out,
365 domain.as_deref(),
366 site_path.as_deref(),
367 id.as_deref(),
368 ),
369 MyceliumAction::Pulse {
370 synapse,
371 synapse_token_secret,
372 file,
373 } => rt.block_on(mycelium::handle_pulse(
374 &out,
375 synapse.as_deref(),
376 synapse_token_secret.as_deref(),
377 &file,
378 )),
379 MyceliumAction::Serve {
380 domain,
381 site_path,
382 port,
383 } => mycelium::handle_serve(&out, domain.as_deref(), site_path.as_deref(), port),
384 },
385
386 Commands::Synapse { action } => match action {
387 SynapseAction::Discover {
388 synapse,
389 synapse_token_secret,
390 } => rt.block_on(synapse::handle_discover(
391 &out,
392 synapse.as_deref(),
393 synapse_token_secret.as_deref(),
394 )),
395 SynapseAction::List => synapse::handle_list(&out),
396 SynapseAction::Health {
397 synapse,
398 synapse_token_secret,
399 } => rt.block_on(synapse::handle_info(
400 &out,
401 synapse.as_deref(),
402 synapse_token_secret.as_deref(),
403 )),
404 SynapseAction::Add { url } => synapse::handle_add(&out, &url),
405 SynapseAction::Remove { domain } => synapse::handle_remove(&out, &domain),
406 SynapseAction::Use { domain } => synapse::handle_use(&out, &domain),
407 SynapseAction::Config {
408 domain,
409 token_secret,
410 } => synapse::handle_config(&out, &domain, token_secret.as_deref()),
411 },
412
413 Commands::Cache { action } => match action {
414 CacheAction::List => cache::handle_list(&out),
415 CacheAction::Clean { all } => cache::handle_clean(&out, all),
416 CacheAction::Path { uri } => cache::handle_path(&out, &uri),
417 },
418
419 Commands::Config { action } => match action {
420 ConfigAction::List => config::handle_list(&out),
421 ConfigAction::Set { key, value } => config::handle_set(&out, &key, &value),
422 },
423
424 Commands::Skill { action } => skill_admin::handle_skill(&out, action),
425 }
426}