Skip to main content

hypha/cli/
run.rs

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