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        sites: Vec<String>,
243        check: bool,
244    },
245}
246
247#[derive(Serialize)]
248#[serde(tag = "action", rename_all = "snake_case")]
249pub enum HatchTreeCommands {
250    Set {
251        algorithm: Option<String>,
252        exclude_names: Option<Vec<String>>,
253        follow_rules: Option<Vec<String>>,
254    },
255    Show,
256}
257
258#[derive(Serialize)]
259#[serde(tag = "action", rename_all = "snake_case")]
260pub enum MyceliumAction {
261    Root {
262        domain: Option<String>,
263        hub: Option<String>,
264        site_path: Option<String>,
265        name: Option<String>,
266        synopsis: Option<String>,
267        bio: Option<String>,
268        #[serde(rename = "endpoints_base_url")]
269        endpoints_base: Option<String>,
270    },
271    Status {
272        domain: Option<String>,
273        site_path: Option<String>,
274        id: Option<String>,
275    },
276    Serve {
277        domain: Option<String>,
278        site_path: Option<String>,
279        port: u16,
280    },
281    Nutrient {
282        #[serde(flatten)]
283        command: NutrientCommands,
284    },
285    Spore {
286        #[serde(flatten)]
287        command: SporeCommands,
288    },
289    Pulse {
290        #[serde(rename = "synapse_selector")]
291        synapse: Option<String>,
292        synapse_token_secret: Option<String>,
293        file: String,
294    },
295}
296
297#[derive(Serialize)]
298#[serde(tag = "action", rename_all = "snake_case")]
299pub enum NutrientCommands {
300    Add {
301        domain: String,
302        method_type: String,
303        with_entries: Vec<String>,
304        site_path: Option<String>,
305    },
306    Remove {
307        domain: String,
308        method_type: String,
309        site_path: Option<String>,
310    },
311    Clear {
312        domain: String,
313        site_path: Option<String>,
314    },
315}
316
317#[derive(Serialize)]
318#[serde(tag = "action", rename_all = "snake_case")]
319pub enum SporeCommands {
320    Yank {
321        id: String,
322        domain: Option<String>,
323        site_path: Option<String>,
324        purge: bool,
325    },
326    Unyank {
327        id: String,
328        domain: Option<String>,
329        site_path: Option<String>,
330    },
331}
332
333#[derive(Serialize)]
334#[serde(tag = "action", rename_all = "snake_case")]
335pub enum SynapseAction {
336    Discover {
337        #[serde(rename = "synapse_selector")]
338        synapse: Option<String>,
339        synapse_token_secret: Option<String>,
340    },
341    List,
342    Health {
343        #[serde(rename = "synapse_selector")]
344        synapse: Option<String>,
345        synapse_token_secret: Option<String>,
346    },
347    Add {
348        #[serde(rename = "synapse_url")]
349        url: String,
350    },
351    Remove {
352        domain: String,
353    },
354    Use {
355        domain: String,
356    },
357    Config {
358        domain: String,
359        token_secret: Option<String>,
360    },
361}
362
363#[derive(Serialize)]
364#[serde(tag = "action", rename_all = "snake_case")]
365pub enum CacheAction {
366    List,
367    Clean {
368        all: bool,
369    },
370    Path {
371        #[serde(rename = "cmn_url")]
372        uri: String,
373    },
374}
375
376#[derive(Serialize)]
377#[serde(tag = "action", rename_all = "snake_case")]
378pub enum ConfigAction {
379    List,
380    Set { key: String, value: String },
381}
382
383#[cfg(test)]
384#[allow(clippy::unwrap_used)]
385mod tests {
386    use super::{Commands, MyceliumAction};
387
388    #[test]
389    fn startup_diagnostic_fields_name_known_urls_canonically() {
390        let release = serde_json::to_value(Commands::Release {
391            domain: "example.com".to_string(),
392            source: None,
393            site_path: None,
394            dist_git: Some(
395                "https://user:password@example.com/repo?token_secret=canary".to_string(),
396            ),
397            dist_ref: None,
398            archive: "zstd".to_string(),
399            dry_run: true,
400        })
401        .unwrap();
402        assert!(release.get("dist_git_url").is_some());
403        assert!(release.get("dist_git").is_none());
404        let release = agent_first_data::redacted_value(&release);
405        let rendered = serde_json::to_string(&release).unwrap();
406        assert!(!rendered.contains("password"));
407        assert!(!rendered.contains("canary"));
408
409        let mycelium = serde_json::to_value(MyceliumAction::Root {
410            domain: None,
411            hub: None,
412            site_path: None,
413            name: None,
414            synopsis: None,
415            bio: None,
416            endpoints_base: Some(
417                "https://user:password@example.com?token_secret=canary".to_string(),
418            ),
419        })
420        .unwrap();
421        assert!(mycelium.get("endpoints_base_url").is_some());
422        assert!(mycelium.get("endpoints_base").is_none());
423        let mycelium = agent_first_data::redacted_value(&mycelium);
424        let rendered = serde_json::to_string(&mycelium).unwrap();
425        assert!(!rendered.contains("password"));
426        assert!(!rendered.contains("canary"));
427    }
428}