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