Skip to main content

hypha/cli/
types.rs

1//! Typed commands produced by the closed-world CLI registry.
2
3use serde::Serialize;
4
5#[derive(Serialize)]
6pub struct Cli {
7    pub output: String,
8    pub output_to: String,
9    pub log: Vec<String>,
10    pub command: Commands,
11}
12
13#[derive(Serialize)]
14#[serde(tag = "command", rename_all = "snake_case")]
15pub enum Commands {
16    Sense {
17        #[serde(rename = "cmn_url")]
18        uri: String,
19        id: Option<String>,
20    },
21    Taste {
22        #[serde(rename = "cmn_url")]
23        uri: String,
24        verdict: Option<substrate::TasteVerdict>,
25        notes: Option<String>,
26        #[serde(rename = "synapse_selector")]
27        synapse: Option<String>,
28        synapse_token_secret: Option<String>,
29        domain: Option<String>,
30    },
31    Spawn {
32        #[serde(rename = "cmn_url")]
33        uri: String,
34        directory: Option<String>,
35        vcs: Option<VcsArg>,
36        dist: Option<DistArg>,
37        bond: bool,
38    },
39    Grow {
40        dist: Option<DistArg>,
41        #[serde(rename = "synapse_selector")]
42        synapse: Option<String>,
43        synapse_token_secret: Option<String>,
44        bond: bool,
45    },
46    Absorb {
47        uris: Vec<String>,
48        discover: bool,
49        #[serde(rename = "synapse_selector")]
50        synapse: Option<String>,
51        synapse_token_secret: Option<String>,
52        max_depth: u32,
53    },
54    Bond {
55        clean: bool,
56        status: bool,
57    },
58    Replicate {
59        uris: Vec<String>,
60        refs: bool,
61        domain: String,
62        site_path: Option<String>,
63    },
64    Hatch {
65        id: Option<String>,
66        version: Option<String>,
67        name: Option<String>,
68        domain: Option<String>,
69        synopsis: Option<String>,
70        intent: Vec<String>,
71        mutations: Vec<String>,
72        license: Option<String>,
73        #[serde(skip)]
74        command: Option<HatchCommands>,
75    },
76    Release {
77        domain: String,
78        source: Option<String>,
79        site_path: Option<String>,
80        #[serde(rename = "dist_git_url")]
81        dist_git: Option<String>,
82        dist_ref: Option<String>,
83        archive: String,
84        dry_run: bool,
85    },
86    Lineage {
87        #[serde(rename = "cmn_url")]
88        uri: String,
89        direction: Option<DirectionArg>,
90        #[serde(rename = "synapse_selector")]
91        synapse: Option<String>,
92        synapse_token_secret: Option<String>,
93        max_depth: u32,
94    },
95    Search {
96        query: String,
97        #[serde(rename = "synapse_selector")]
98        synapse: Option<String>,
99        synapse_token_secret: Option<String>,
100        domain: Option<String>,
101        license: Option<String>,
102        bonds: Option<String>,
103        limit: u32,
104    },
105    Mycelium {
106        #[serde(flatten)]
107        action: MyceliumAction,
108    },
109    Synapse {
110        #[serde(flatten)]
111        action: SynapseAction,
112    },
113    Cache {
114        #[serde(flatten)]
115        action: CacheAction,
116    },
117    Config {
118        #[serde(flatten)]
119        action: ConfigAction,
120    },
121    Skill {
122        #[serde(flatten)]
123        action: SkillCommand,
124    },
125}
126
127#[derive(Serialize)]
128#[serde(tag = "action", rename_all = "snake_case")]
129pub enum SkillCommand {
130    Status(SkillOptionsArg),
131    Install(SkillOptionsArg),
132    Uninstall(SkillOptionsArg),
133}
134
135#[derive(Serialize)]
136pub struct SkillOptionsArg {
137    pub agent: SkillAgentArg,
138    pub scope: SkillScopeArg,
139    pub skills_dir: Option<String>,
140    pub force: bool,
141}
142
143#[derive(Clone, Copy, Debug, Serialize)]
144#[serde(rename_all = "kebab-case")]
145pub enum SkillAgentArg {
146    All,
147    Codex,
148    ClaudeCode,
149    Opencode,
150}
151
152#[derive(Clone, Copy, Debug, Serialize)]
153#[serde(rename_all = "lowercase")]
154pub enum SkillScopeArg {
155    Personal,
156    Project,
157}
158
159#[derive(Clone, Copy, Debug, Serialize)]
160#[serde(rename_all = "lowercase")]
161pub enum DistArg {
162    Archive,
163    Git,
164}
165
166impl DistArg {
167    pub fn as_str(self) -> &'static str {
168        match self {
169            Self::Archive => "archive",
170            Self::Git => "git",
171        }
172    }
173}
174
175#[derive(Clone, Copy, Debug, Serialize)]
176#[serde(rename_all = "lowercase")]
177pub enum VcsArg {
178    Git,
179    None,
180}
181
182impl VcsArg {
183    pub fn as_str(self) -> &'static str {
184        match self {
185            Self::Git => "git",
186            Self::None => "none",
187        }
188    }
189}
190
191#[derive(Clone, Copy, Debug, Serialize)]
192#[serde(rename_all = "lowercase")]
193pub enum DirectionArg {
194    In,
195    Out,
196}
197
198impl DirectionArg {
199    pub fn as_str(self) -> &'static str {
200        match self {
201            Self::In => "in",
202            Self::Out => "out",
203        }
204    }
205}
206
207#[derive(Serialize)]
208#[serde(tag = "action", rename_all = "snake_case")]
209pub enum HatchCommands {
210    Bond {
211        #[serde(flatten)]
212        command: HatchBondCommands,
213    },
214    Tree {
215        #[serde(flatten)]
216        command: HatchTreeCommands,
217    },
218}
219
220#[derive(Serialize)]
221#[serde(tag = "action", rename_all = "snake_case")]
222pub enum HatchBondCommands {
223    Set {
224        #[serde(rename = "cmn_url")]
225        uri: String,
226        relation: Option<substrate::BondRelation>,
227        id: Option<String>,
228        reason: Option<String>,
229        with_entries: Vec<String>,
230    },
231    Remove {
232        #[serde(rename = "cmn_url")]
233        uri: Option<String>,
234        relation: Option<substrate::BondRelation>,
235    },
236    Clear,
237    Sync {
238        relation: substrate::BondRelation,
239        spec: String,
240        domain: Option<String>,
241        site_path: Option<String>,
242        check: bool,
243    },
244}
245
246#[derive(Serialize)]
247#[serde(tag = "action", rename_all = "snake_case")]
248pub enum HatchTreeCommands {
249    Set {
250        algorithm: Option<String>,
251        exclude_names: Option<Vec<String>>,
252        follow_rules: Option<Vec<String>>,
253    },
254    Show,
255}
256
257#[derive(Serialize)]
258#[serde(tag = "action", rename_all = "snake_case")]
259pub enum MyceliumAction {
260    Root {
261        domain: Option<String>,
262        hub: Option<String>,
263        site_path: Option<String>,
264        name: Option<String>,
265        synopsis: Option<String>,
266        bio: Option<String>,
267        #[serde(rename = "endpoints_base_url")]
268        endpoints_base: Option<String>,
269    },
270    Status {
271        domain: Option<String>,
272        site_path: Option<String>,
273        id: Option<String>,
274    },
275    Serve {
276        domain: Option<String>,
277        site_path: Option<String>,
278        port: u16,
279    },
280    Nutrient {
281        #[serde(flatten)]
282        command: NutrientCommands,
283    },
284    Spore {
285        #[serde(flatten)]
286        command: SporeCommands,
287    },
288    Pulse {
289        #[serde(rename = "synapse_selector")]
290        synapse: Option<String>,
291        synapse_token_secret: Option<String>,
292        file: String,
293    },
294}
295
296#[derive(Serialize)]
297#[serde(tag = "action", rename_all = "snake_case")]
298pub enum NutrientCommands {
299    Add {
300        domain: String,
301        method_type: String,
302        with_entries: Vec<String>,
303        site_path: Option<String>,
304    },
305    Remove {
306        domain: String,
307        method_type: String,
308        site_path: Option<String>,
309    },
310    Clear {
311        domain: String,
312        site_path: Option<String>,
313    },
314}
315
316#[derive(Serialize)]
317#[serde(tag = "action", rename_all = "snake_case")]
318pub enum SporeCommands {
319    Yank {
320        id: String,
321        domain: Option<String>,
322        site_path: Option<String>,
323        purge: bool,
324    },
325    Unyank {
326        id: String,
327        domain: Option<String>,
328        site_path: Option<String>,
329    },
330}
331
332#[derive(Serialize)]
333#[serde(tag = "action", rename_all = "snake_case")]
334pub enum SynapseAction {
335    Discover {
336        #[serde(rename = "synapse_selector")]
337        synapse: Option<String>,
338        synapse_token_secret: Option<String>,
339    },
340    List,
341    Health {
342        #[serde(rename = "synapse_selector")]
343        synapse: Option<String>,
344        synapse_token_secret: Option<String>,
345    },
346    Add {
347        #[serde(rename = "synapse_url")]
348        url: String,
349    },
350    Remove {
351        domain: String,
352    },
353    Use {
354        domain: String,
355    },
356    Config {
357        domain: String,
358        token_secret: Option<String>,
359    },
360}
361
362#[derive(Serialize)]
363#[serde(tag = "action", rename_all = "snake_case")]
364pub enum CacheAction {
365    List,
366    Clean {
367        all: bool,
368    },
369    Path {
370        #[serde(rename = "cmn_url")]
371        uri: String,
372    },
373}
374
375#[derive(Serialize)]
376#[serde(tag = "action", rename_all = "snake_case")]
377pub enum ConfigAction {
378    List,
379    Set { key: String, value: String },
380}
381
382#[cfg(test)]
383#[allow(clippy::unwrap_used)]
384mod tests {
385    use super::{Commands, MyceliumAction};
386
387    #[test]
388    fn startup_diagnostic_fields_name_known_urls_canonically() {
389        let release = serde_json::to_value(Commands::Release {
390            domain: "example.com".to_string(),
391            source: None,
392            site_path: None,
393            dist_git: Some(
394                "https://user:password@example.com/repo?token_secret=canary".to_string(),
395            ),
396            dist_ref: None,
397            archive: "zstd".to_string(),
398            dry_run: true,
399        })
400        .unwrap();
401        assert!(release.get("dist_git_url").is_some());
402        assert!(release.get("dist_git").is_none());
403        let release = agent_first_data::redacted_value(&release);
404        let rendered = serde_json::to_string(&release).unwrap();
405        assert!(!rendered.contains("password"));
406        assert!(!rendered.contains("canary"));
407
408        let mycelium = serde_json::to_value(MyceliumAction::Root {
409            domain: None,
410            hub: None,
411            site_path: None,
412            name: None,
413            synopsis: None,
414            bio: None,
415            endpoints_base: Some(
416                "https://user:password@example.com?token_secret=canary".to_string(),
417            ),
418        })
419        .unwrap();
420        assert!(mycelium.get("endpoints_base_url").is_some());
421        assert!(mycelium.get("endpoints_base").is_none());
422        let mycelium = agent_first_data::redacted_value(&mycelium);
423        let rendered = serde_json::to_string(&mycelium).unwrap();
424        assert!(!rendered.contains("password"));
425        assert!(!rendered.contains("canary"));
426    }
427}