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                    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}