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