Skip to main content

hypha/cli/
run.rs

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}