1use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
7use serde::Serialize;
8
9mod run;
10
11pub use run::execute;
12
13#[derive(Parser)]
14#[command(name = "hypha")]
15#[command(version)]
16#[command(about = "CMN Client - A bio-digital extension for Visitors to release and absorb Spores")]
17#[command(disable_help_subcommand = true)]
18#[command(after_long_help = concat!(
19 "All output follows Agent-First Data format:\n",
20 " {\"kind\": \"result\", \"result\": {...}, \"trace\": {...}}\n",
21 "\n",
22 "Quick start (try with cmn.dev):\n",
23 " hypha sense cmn://cmn.dev\n",
24 " hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
25 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
26 " hypha cache list\n",
27 "\n",
28 "More help:\n",
29 " hypha <command> --help Show one command layer\n",
30 " hypha --help --recursive Expand every command and flag\n",
31 " hypha --help --recursive --output markdown\n",
32 " Generate recursive Markdown reference",
33))]
34pub struct Cli {
35 #[arg(short, long, default_value = "json", global = true)]
37 pub output: String,
38
39 #[arg(long, value_delimiter = ',', global = true)]
41 pub log: Vec<String>,
42
43 #[arg(long, value_name = "PATH", global = true)]
45 pub stdout_file: Option<std::path::PathBuf>,
46
47 #[arg(long, value_name = "PATH", global = true)]
49 pub stderr_file: Option<std::path::PathBuf>,
50
51 #[command(subcommand)]
52 pub command: Commands,
53}
54
55#[derive(Subcommand, Serialize)]
56#[serde(tag = "command", rename_all = "snake_case")]
57pub enum Commands {
58 #[command(after_long_help = concat!(
63 "URI types:\n",
64 " cmn://DOMAIN List all spores on a site\n",
65 " cmn://DOMAIN/HASH View a specific spore\n",
66 " cmn://DOMAIN --id SPORE_ID View latest spore with id from a site\n",
67 "\n",
68 "Examples:\n",
69 " hypha sense cmn://cmn.dev\n",
70 " hypha sense cmn://cmn.dev --id cmn-spec\n",
71 " hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
72 " hypha sense cmn://cmn.dev -o yaml",
73 ))]
74 Sense {
75 uri: String,
77 #[arg(long)]
79 id: Option<String>,
80 },
81
82 #[command(after_long_help = concat!(
84 "Without --verdict: downloads the spore for local review.\n",
85 "With --verdict: records a verdict (sweet, fresh, safe, rotten, toxic).\n",
86 "\n",
87 "Examples:\n",
88 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
89 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
90 " --verdict safe --notes \"Reviewed: clean code\"\n",
91 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
92 " --verdict safe --domain cmn.dev --synapse https://synapse.cmn.dev",
93 ))]
94 Taste {
95 uri: String,
97 #[arg(long, value_name = "VERDICT")]
99 verdict: Option<substrate::TasteVerdict>,
100 #[arg(long)]
102 notes: Option<String>,
103 #[arg(long)]
105 synapse: Option<String>,
106 #[arg(long)]
108 synapse_token_secret: Option<String>,
109 #[arg(long)]
111 domain: Option<String>,
112 },
113
114 #[command(after_long_help = concat!(
116 "Distribution sources (auto-detected):\n",
117 " archive Download .tar.zst archive (default, fastest)\n",
118 " git Clone from dist.git URL if available\n",
119 "\n",
120 "Examples:\n",
121 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
122 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
123 " my-project --vcs git\n",
124 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
125 " --dist git",
126 ))]
127 Spawn {
128 uri: String,
130 directory: Option<String>,
132 #[arg(long, value_enum, value_name = "TYPE")]
134 vcs: Option<VcsArg>,
135 #[arg(long, value_enum, value_name = "SOURCE")]
137 dist: Option<DistArg>,
138 #[arg(long)]
140 bond: bool,
141 },
142
143 #[command(after_long_help = "\
145Run inside a previously spawned directory to update it.
146Uses Synapse lineage to discover newer versions from the same publisher.
147
148If local files have been modified (git dirty or tree hash mismatch),
149grow refuses to overwrite them and shows the cache path for manual merge.
150
151Examples:
152 hypha grow
153 hypha grow --synapse synapse.cmn.dev
154 hypha grow --dist git
155 hypha grow --dist archive
156 hypha grow --bond --synapse synapse.cmn.dev")]
157 Grow {
158 #[arg(long, value_enum, value_name = "SOURCE")]
160 dist: Option<DistArg>,
161 #[arg(long)]
163 synapse: Option<String>,
164 #[arg(long)]
166 synapse_token_secret: Option<String>,
167 #[arg(long)]
169 bond: bool,
170 },
171
172 #[command(after_long_help = concat!(
174 "Absorb downloads spores into .cmn/absorb/ for AI-assisted merge.\n",
175 "Use --discover to auto-discover descendants via Synapse.\n",
176 "\n",
177 "Examples:\n",
178 " hypha absorb cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
179 " hypha absorb --discover --synapse https://synapse.cmn.dev\n",
180 " hypha absorb --discover --synapse https://synapse.cmn.dev --max-depth 5",
181 ))]
182 Absorb {
183 #[arg(required_unless_present = "discover")]
185 uris: Vec<String>,
186 #[arg(long)]
188 discover: bool,
189 #[arg(long)]
191 synapse: Option<String>,
192 #[arg(long)]
194 synapse_token_secret: Option<String>,
195 #[arg(long, default_value = "10")]
197 max_depth: u32,
198 },
199
200 #[command(after_long_help = "\
202Examples:
203 hypha bond
204 hypha bond --status
205 hypha bond --clean")]
206 Bond {
207 #[arg(long)]
209 clean: bool,
210 #[arg(long)]
212 status: bool,
213 },
214
215 #[command(after_long_help = "\
217Replicates spores from another domain to yours. The hash stays the same
218because core + core_signature are preserved. Only capsule_signature changes.
219
220Examples:
221 hypha replicate cmn://other.dev/HASH --domain my.dev
222 hypha replicate --refs --domain my.dev")]
223 Replicate {
224 #[arg(required_unless_present = "refs")]
226 uris: Vec<String>,
227 #[arg(long)]
229 refs: bool,
230 #[arg(long)]
232 domain: String,
233 #[arg(long)]
235 site_path: Option<String>,
236 },
237
238 #[command(after_long_help = "\
240Examples:
241 hypha hatch --id my-tool --name \"My Tool\" --synopsis \"A useful tool\"
242 hypha hatch --intent \"Provide a reusable HTTP client for CMN agents\" --mutations \"Initial release\"
243 hypha hatch --license MIT --domain cmn.dev
244
245Subcommands:
246 hypha hatch bond set/remove/clear/sync Manage bonds in spore.core.json
247 hypha hatch tree set/show Manage tree configuration")]
248 #[command(args_conflicts_with_subcommands = true)]
249 Hatch {
250 #[arg(long)]
252 id: Option<String>,
253 #[arg(long)]
255 version: Option<String>,
256 #[arg(long)]
258 name: Option<String>,
259 #[arg(long)]
261 domain: Option<String>,
262 #[arg(long)]
264 synopsis: Option<String>,
265 #[arg(long)]
267 intent: Vec<String>,
268 #[arg(long)]
270 mutations: Vec<String>,
271 #[arg(long)]
273 license: Option<String>,
274
275 #[command(subcommand)]
276 #[serde(skip)]
277 command: Option<HatchCommands>,
278 },
279
280 #[command(after_long_help = "\
282Requires `hypha mycelium root` first to set up the site.
283
284Examples:
285 hypha release --domain cmn.dev
286 hypha release --domain cmn.dev --source ./my-spore
287 hypha release --domain cmn.dev --dry-run # pre-compute URI without releasing
288 hypha release --domain cmn.dev --archive zstd
289 hypha release --domain cmn.dev --dist-git https://github.com/user/repo --dist-ref v1.0")]
290 Release {
291 #[arg(long)]
293 domain: String,
294 #[arg(long)]
296 source: Option<String>,
297 #[arg(long)]
299 site_path: Option<String>,
300 #[arg(long)]
302 dist_git: Option<String>,
303 #[arg(long)]
305 dist_ref: Option<String>,
306 #[arg(long, value_name = "FORMAT", default_value = "zstd")]
308 archive: String,
309 #[arg(long)]
311 dry_run: bool,
312 },
313
314 #[command(after_long_help = concat!(
319 "Direction:\n",
320 " --direction in Find descendants / forks (default)\n",
321 " --direction out Trace ancestors / spawn chain\n",
322 "\n",
323 "Examples:\n",
324 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
325 " --synapse https://synapse.cmn.dev\n",
326 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
327 " --direction out --synapse https://synapse.cmn.dev\n",
328 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
329 " --synapse https://synapse.cmn.dev --max-depth 5",
330 ))]
331 Lineage {
332 uri: String,
334 #[arg(long, value_enum)]
336 direction: Option<DirectionArg>,
337 #[arg(long)]
339 synapse: Option<String>,
340 #[arg(long)]
342 synapse_token_secret: Option<String>,
343 #[arg(long, default_value = "10")]
345 max_depth: u32,
346 },
347
348 #[command(after_long_help = "\
350Examples:
351 hypha search \"protocol spec\" --synapse https://synapse.cmn.dev
352 hypha search \"data format\" --synapse https://synapse.cmn.dev --domain cmn.dev
353 hypha search \"agent tools\" --synapse https://synapse.cmn.dev --license MIT --limit 5
354 hypha search \"http client\" --bonds spawned_from:cmn://cmn.dev/b3.abc123")]
355 Search {
356 query: String,
358 #[arg(long)]
360 synapse: Option<String>,
361 #[arg(long)]
363 synapse_token_secret: Option<String>,
364 #[arg(long)]
366 domain: Option<String>,
367 #[arg(long)]
369 license: Option<String>,
370 #[arg(long)]
372 bonds: Option<String>,
373 #[arg(long, default_value = "20")]
375 limit: u32,
376 },
377
378 Mycelium {
383 #[command(subcommand)]
384 #[serde(flatten)]
385 action: MyceliumAction,
386 },
387
388 Synapse {
390 #[command(subcommand)]
391 #[serde(flatten)]
392 action: SynapseAction,
393 },
394
395 Cache {
397 #[command(subcommand)]
398 #[serde(flatten)]
399 action: CacheAction,
400 },
401
402 Config {
404 #[command(subcommand)]
405 #[serde(flatten)]
406 action: ConfigAction,
407 },
408
409 #[command(after_long_help = "\
411Examples:
412 hypha skill status
413 hypha skill install --agent codex
414 hypha skill install --agent claude-code --scope project
415 hypha skill uninstall --agent opencode --skills-dir /tmp/skills --force")]
416 Skill {
417 #[command(subcommand)]
418 #[serde(flatten)]
419 action: SkillCommand,
420 },
421}
422
423#[derive(Subcommand, Serialize)]
424#[serde(tag = "action", rename_all = "snake_case")]
425pub enum SkillCommand {
426 Status(SkillOptionsArg),
428 Install(SkillOptionsArg),
430 Uninstall(SkillOptionsArg),
432}
433
434#[derive(Args, Serialize)]
435pub struct SkillOptionsArg {
436 #[arg(long, value_enum, default_value = "all")]
438 pub agent: SkillAgentArg,
439 #[arg(long, value_enum, default_value = "personal")]
441 pub scope: SkillScopeArg,
442 #[arg(long)]
444 pub skills_dir: Option<String>,
445 #[arg(long)]
447 pub force: bool,
448}
449
450#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
451#[serde(rename_all = "kebab-case")]
452pub enum SkillAgentArg {
453 All,
454 Codex,
455 ClaudeCode,
456 Opencode,
457}
458
459#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
460#[serde(rename_all = "lowercase")]
461pub enum SkillScopeArg {
462 Personal,
463 Project,
464}
465
466#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
468#[serde(rename_all = "lowercase")]
469pub enum DistArg {
470 Archive,
471 Git,
472}
473
474impl DistArg {
475 pub fn as_str(self) -> &'static str {
477 match self {
478 Self::Archive => "archive",
479 Self::Git => "git",
480 }
481 }
482}
483
484#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
486#[serde(rename_all = "lowercase")]
487pub enum VcsArg {
488 Git,
489 None,
490}
491
492impl VcsArg {
493 pub fn as_str(self) -> &'static str {
495 match self {
496 Self::Git => "git",
497 Self::None => "none",
498 }
499 }
500}
501
502#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
504#[serde(rename_all = "lowercase")]
505pub enum DirectionArg {
506 In,
507 Out,
508}
509
510impl DirectionArg {
511 pub fn as_str(self) -> &'static str {
513 match self {
514 Self::In => "in",
515 Self::Out => "out",
516 }
517 }
518}
519
520#[derive(Subcommand, Serialize)]
521#[serde(tag = "action", rename_all = "snake_case")]
522pub enum HatchCommands {
523 #[command(after_long_help = "\
525Examples:
526 hypha hatch bond set --uri cmn://cmn.dev/b3.abc --relation follows --id my-lib --reason \"Core library\"
527 hypha hatch bond set --uri cmn://cmn.dev/b3.abc --with 'mints=[\"https://mint.example.com\"]'
528 hypha hatch bond remove --relation follows
529 hypha hatch bond clear
530 hypha hatch bond sync --relation follows --spec ./follows.json --domain cmn.dev
531 hypha hatch bond sync --relation follows --spec - --check")]
532 Bond {
533 #[command(subcommand)]
534 #[serde(flatten)]
535 command: HatchBondCommands,
536 },
537 Tree {
539 #[command(subcommand)]
540 #[serde(flatten)]
541 command: HatchTreeCommands,
542 },
543}
544
545#[derive(Subcommand, Serialize)]
546#[serde(tag = "action", rename_all = "snake_case")]
547pub enum HatchBondCommands {
548 Set {
550 #[arg(long)]
552 uri: String,
553 #[arg(long)]
555 relation: Option<substrate::BondRelation>,
556 #[arg(long)]
558 id: Option<String>,
559 #[arg(long)]
561 reason: Option<String>,
562 #[arg(long = "with", value_name = "KEY=VALUE")]
564 with_entries: Vec<String>,
565 },
566 Remove {
568 #[arg(long)]
570 uri: Option<String>,
571 #[arg(long)]
573 relation: Option<substrate::BondRelation>,
574 },
575 Clear,
577 #[command(after_long_help = "\
579Spec format: a JSON array of {id, reason?, with?, uri?} objects (or \"-\" for stdin):
580 [
581 {\"id\": \"my-lib\", \"reason\": \"Core library\", \"uri\": \"cmn://cmn.dev/b3.abc\"},
582 {\"id\": \"other-lib\", \"reason\": \"Also used\", \"with\": {\"pinned\": true}}
583 ]
584
585Entries that omit \"uri\" are resolved internally: deployed mycelium inventory
586first, `release --dry-run` fallback for not-yet-deployed siblings (requires
587--domain; --site-path for a non-default site). An explicit \"uri\" skips
588resolution entirely. Resolution reads live deploy state, so sync is only
589idempotent relative to a fixed deploy point: run `release` for all siblings
590before `sync`, not interleaved with it.
591
592After a successful run, this relation's bonds exactly equal the spec, in spec
593order; every other relation is left untouched. `with` is replaced wholesale
594per entry (not merged) — same as `hatch bond set`.
595
596`--check` computes the add/update/remove diff without writing anything. If
597there is any drift it exits 7 (distinct from the normal error exit 1) so CI
598or a release pipeline can gate on it; with no drift it exits 0.
599
600Reading bonds is a job for afdata (`value`/`get`/`paths` on spore.core.json),
601not hypha — hatch is write-only, there is no `bond show`.
602
603Examples:
604 hypha hatch bond sync --relation follows --spec ./follows.json --domain cmn.dev
605 hypha hatch bond sync --relation follows --spec - --domain cmn.dev --site-path ./deploy/cmn.dev
606 hypha hatch bond sync --relation follows --spec ./follows.json --check
607 hypha hatch bond sync --relation depends_on --spec '[]' # clears the relation")]
608 Sync {
609 #[arg(long)]
611 relation: substrate::BondRelation,
612 #[arg(long)]
614 spec: String,
615 #[arg(long)]
617 domain: Option<String>,
618 #[arg(long)]
620 site_path: Option<String>,
621 #[arg(long)]
623 check: bool,
624 },
625}
626
627#[derive(Subcommand, Serialize)]
628#[serde(tag = "action", rename_all = "snake_case")]
629pub enum HatchTreeCommands {
630 Set {
632 #[arg(long)]
634 algorithm: Option<String>,
635 #[arg(long, num_args = 1..)]
637 exclude_names: Option<Vec<String>>,
638 #[arg(long, num_args = 1..)]
640 follow_rules: Option<Vec<String>>,
641 },
642 Show,
644}
645
646#[derive(Subcommand, Serialize)]
647#[serde(tag = "action", rename_all = "snake_case")]
648pub enum MyceliumAction {
649 #[command(after_long_help = "\
651Creates ~/.cmn/mycelium/<domain>/ with key pair and site structure.
652Run this once before `hypha release`.
653
654With --hub, creates a taste-only account on a hosted hub (e.g. cmnhub.com):
655 1. Generates ed25519 key pair
656 2. Computes subdomain from pubkey (ed-<base32>.hub)
657 3. Creates taste-only cmn.json with taste endpoint
658 4. Registers hub as a synapse node
659 5. Sets [defaults.taste] so `hypha taste` auto-submits
660
661After --hub, register with the hub then taste without extra flags:
662 curl -X POST https://cmnhub.com/synapse/pulse -H 'Content-Type: application/json' \\
663 -d @~/.cmn/mycelium/ed-xxx.cmnhub.com/public/.well-known/cmn.json
664 hypha taste cmn://example.com/b3.HASH --verdict safe
665
666Examples:
667 hypha mycelium root cmn.dev --name \"CMN\" --synopsis \"Code Mycelial Network\"
668 hypha mycelium root cmn.dev --endpoints-base https://cmn.dev
669 hypha mycelium root example.com --site-path /custom/path
670 hypha mycelium root --hub cmnhub.com")]
671 Root {
672 domain: Option<String>,
674 #[arg(long, conflicts_with = "endpoints_base")]
677 hub: Option<String>,
678 #[arg(long)]
680 site_path: Option<String>,
681 #[arg(long)]
683 name: Option<String>,
684 #[arg(long)]
686 synopsis: Option<String>,
687 #[arg(long)]
689 bio: Option<String>,
690 #[arg(long)]
692 endpoints_base: Option<String>,
693 },
694 #[command(after_long_help = "\
696Examples:
697 hypha mycelium status
698 hypha mycelium status cmn.dev
699 hypha mycelium status cmn.dev --id cmn-spec --site-path deploy/cmn.dev")]
700 Status {
701 domain: Option<String>,
703 #[arg(long)]
705 site_path: Option<String>,
706 #[arg(long)]
708 id: Option<String>,
709 },
710 #[command(after_long_help = "\
712Examples:
713 hypha mycelium serve
714 hypha mycelium serve cmn.dev --port 3000")]
715 Serve {
716 domain: Option<String>,
718 #[arg(long)]
720 site_path: Option<String>,
721 #[arg(long, default_value = "8080")]
723 port: u16,
724 },
725 #[command(after_long_help = "\
727Examples:
728 hypha mycelium nutrient add cmn.dev --type lightning_address --with address=user@example.com
729 hypha mycelium nutrient add cmn.dev --type url --with url=https://example.com --with label=Donate
730 hypha mycelium nutrient remove cmn.dev --type url
731 hypha mycelium nutrient clear cmn.dev")]
732 Nutrient {
733 #[command(subcommand)]
734 #[serde(flatten)]
735 command: NutrientCommands,
736 },
737 #[command(after_long_help = "\
739Adding a spore is not here — that happens as a side effect of `hypha release`.
740These manage the post-publish lifecycle of a spore already in the inventory.
741
742Examples:
743 hypha mycelium spore yank --id afconfig --site-path deploy/cmn.dev
744 hypha mycelium spore yank --id afconfig --purge
745 hypha mycelium spore unyank --id afconfig --site-path deploy/cmn.dev")]
746 Spore {
747 #[command(subcommand)]
748 #[serde(flatten)]
749 command: SporeCommands,
750 },
751 #[command(after_long_help = "\
753Examples:
754 hypha mycelium pulse --synapse synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json
755 hypha mycelium pulse --synapse https://synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json")]
756 Pulse {
757 #[arg(long)]
759 synapse: Option<String>,
760 #[arg(long)]
762 synapse_token_secret: Option<String>,
763 #[arg(long)]
765 file: String,
766 },
767}
768
769#[derive(Subcommand, Serialize)]
770#[serde(tag = "action", rename_all = "snake_case")]
771pub enum NutrientCommands {
772 Add {
774 domain: String,
776 #[arg(long = "type", value_name = "TYPE")]
778 method_type: String,
779 #[arg(long = "with", value_name = "KEY=VALUE")]
781 with_entries: Vec<String>,
782 #[arg(long)]
784 site_path: Option<String>,
785 },
786 Remove {
788 domain: String,
790 #[arg(long = "type", value_name = "TYPE")]
792 method_type: String,
793 #[arg(long)]
795 site_path: Option<String>,
796 },
797 Clear {
799 domain: String,
801 #[arg(long)]
803 site_path: Option<String>,
804 },
805}
806
807#[derive(Subcommand, Serialize)]
808#[serde(tag = "action", rename_all = "snake_case")]
809pub enum SporeCommands {
810 #[command(after_long_help = "\
812Removes the spore's entry from the domain's mycelium listing and re-signs the
813mycelium + cmn.json. Only the mycelium capsule changes — every other spore
814capsule is untouched, so this is far cheaper than `release --clean-published`.
815
816The domain is inferred from the site's cmn.json; pass --site-path (or --domain
817to pick among multiple ~/.cmn sites). By default the spore's manifest and
818archive stay published so existing `cmn://<domain>/<hash>` references keep
819resolving (crates.io `yank` semantics); --purge also deletes those files.
820
821Examples:
822 hypha mycelium spore yank --id afconfig --site-path deploy/cmn.dev
823 hypha mycelium spore yank --id afconfig --purge")]
824 Yank {
825 #[arg(long)]
827 id: String,
828 #[arg(long)]
830 domain: Option<String>,
831 #[arg(long)]
833 site_path: Option<String>,
834 #[arg(long)]
836 purge: bool,
837 },
838 #[command(after_long_help = "\
840Re-adds a yanked spore to the inventory using its still-published manifest, then
841re-signs. Fails if the manifest was purged (run `hypha release` instead) or the
842id is already listed.
843
844Examples:
845 hypha mycelium spore unyank --id afconfig --site-path deploy/cmn.dev")]
846 Unyank {
847 #[arg(long)]
849 id: String,
850 #[arg(long)]
852 domain: Option<String>,
853 #[arg(long)]
855 site_path: Option<String>,
856 },
857}
858
859#[derive(Subcommand, Serialize)]
860#[serde(tag = "action", rename_all = "snake_case")]
861pub enum SynapseAction {
862 #[command(after_long_help = "\
864Examples:
865 hypha synapse discover
866 hypha synapse discover --synapse https://synapse.cmn.dev")]
867 Discover {
868 #[arg(long)]
870 synapse: Option<String>,
871 #[arg(long)]
873 synapse_token_secret: Option<String>,
874 },
875 #[command(after_long_help = "\
877Examples:
878 hypha synapse list")]
879 List,
880 #[command(after_long_help = "\
882Examples:
883 hypha synapse health
884 hypha synapse health synapse.cmn.dev
885 hypha synapse health https://synapse.cmn.dev")]
886 Health {
887 synapse: Option<String>,
889 #[arg(long)]
891 synapse_token_secret: Option<String>,
892 },
893 #[command(after_long_help = "\
895Examples:
896 hypha synapse add https://synapse.cmn.dev")]
897 Add {
898 url: String,
900 },
901 #[command(after_long_help = "\
903Examples:
904 hypha synapse remove synapse.cmn.dev")]
905 Remove {
906 domain: String,
908 },
909 #[command(after_long_help = "\
911Examples:
912 hypha synapse use synapse.cmn.dev")]
913 Use {
914 domain: String,
916 },
917 #[command(after_long_help = "\
919Examples:
920 hypha synapse config synapse.cmn.dev --token-secret sk-abc123
921 hypha synapse config synapse.cmn.dev --token-secret \"\" # clear token")]
922 Config {
923 domain: String,
925 #[arg(long)]
927 token_secret: Option<String>,
928 },
929}
930
931#[derive(Subcommand, Serialize)]
932#[serde(tag = "action", rename_all = "snake_case")]
933pub enum CacheAction {
934 #[command(after_long_help = "\
936Examples:
937 hypha cache list
938 hypha cache list -o yaml")]
939 List,
940 #[command(after_long_help = "\
942Examples:
943 hypha cache clean --all")]
944 Clean {
945 #[arg(long)]
947 all: bool,
948 },
949 #[command(after_long_help = concat!(
951 "Examples:\n",
952 " hypha cache path cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
953 ))]
954 Path {
955 uri: String,
957 },
958}
959
960#[derive(Subcommand, Serialize)]
961#[serde(tag = "action", rename_all = "snake_case")]
962pub enum ConfigAction {
963 #[command(after_long_help = "\
965Examples:
966 hypha config list
967 hypha config list -o yaml")]
968 List,
969 #[command(after_long_help = "\
971Dotted keys map to TOML sections:
972 cache.path Custom cache directory
973 cache.cmn_ttl_s cmn.json cache TTL in seconds
974 cache.key_trust_ttl_s Key trust cache TTL in seconds
975 cache.key_trust_refresh_mode
976 Key trust refresh mode: expired | always | offline
977 cache.key_trust_synapse_witness_mode
978 Key trust fallback when domain is offline: allow | require_domain
979 cache.spore_max_download_bytes
980 Max spore archive download bytes
981 cache.spore_max_extract_bytes
982 Max total bytes extracted from a spore archive
983 cache.spore_max_extract_files
984 Max files extracted from a spore archive
985 cache.spore_max_extract_file_bytes
986 Max bytes extracted for one spore archive file
987 cache.spore_reject_path_components
988 TOML string array of protected received path components
989 cache.clock_skew_tolerance_s
990 Clock skew tolerance in seconds for key trust TTL (default: 300)
991 defaults.synapse Default synapse domain
992 defaults.domain Default domain for publishing (release)
993 defaults.taste.synapse
994 Synapse to submit taste reports to (overrides defaults.synapse for taste)
995 defaults.taste.domain Domain to sign taste reports with (overrides defaults.domain for taste)
996
997Examples:
998 hypha config set cache.cmn_ttl_s 600
999 hypha config set cache.key_trust_ttl_s 604800
1000 hypha config set cache.key_trust_refresh_mode offline
1001 hypha config set cache.key_trust_synapse_witness_mode require_domain
1002 hypha config set cache.spore_max_download_bytes 1073741824
1003 hypha config set cache.spore_reject_path_components '[\".git\", \".cmn\"]'
1004 hypha config set cache.path /tmp/hypha-cache
1005 hypha config set defaults.synapse synapse.cmn.dev
1006 hypha config set defaults.taste.synapse cmnhub.com
1007 hypha config set defaults.taste.domain ed-xxx.cmnhub.com")]
1008 Set {
1009 key: String,
1011 value: String,
1013 },
1014}
1015
1016pub(crate) fn render_cli_json(value: &serde_json::Value) -> String {
1018 agent_first_data::render(
1019 value,
1020 agent_first_data::OutputFormat::Json,
1021 &agent_first_data::OutputOptions::default(),
1022 )
1023}
1024
1025pub(crate) fn cli_error_value(message: &str, hint: &str) -> serde_json::Value {
1029 match agent_first_data::json_error("invalid_request", message)
1030 .hint(hint)
1031 .trace(serde_json::json!({ "duration_ms": 0 }))
1032 .build()
1033 {
1034 Ok(event) => event.into_value(),
1035 Err(err) => serde_json::json!({
1036 "kind": "error",
1037 "error": {
1038 "code": "internal_error",
1039 "message": err.to_string(),
1040 "retryable": false,
1041 },
1042 "trace": {},
1043 }),
1044 }
1045}
1046
1047pub fn parse_or_exit() -> Cli {
1048 let raw: Vec<String> = std::env::args().collect();
1049
1050 match agent_first_data::cli_handle_version_or_continue(&raw, "hypha", env!("CARGO_PKG_VERSION"))
1051 {
1052 Ok(Some(version)) => {
1053 let mut stdout = std::io::stdout();
1054 let _ = std::io::Write::write_all(&mut stdout, version.as_bytes());
1055 std::process::exit(0);
1056 }
1057 Ok(None) => {}
1058 Err(err) => {
1059 let mut stdout = std::io::stdout();
1060 let message = render_cli_json(err.as_value());
1061 let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1062 let _ = std::io::Write::write_all(&mut stdout, b"\n");
1063 std::process::exit(2);
1064 }
1065 }
1066
1067 match agent_first_data::cli_handle_help_or_continue(
1068 &raw,
1069 &Cli::command(),
1070 &agent_first_data::HelpConfig::human_cli_default(),
1071 ) {
1072 Ok(Some(help)) => {
1073 let mut stdout = std::io::stdout();
1074 let _ = std::io::Write::write_all(&mut stdout, help.as_bytes());
1075 std::process::exit(0);
1076 }
1077 Ok(None) => {}
1078 Err(err) => {
1079 let mut stdout = std::io::stdout();
1080 let message = render_cli_json(&err);
1081 let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1082 let _ = std::io::Write::write_all(&mut stdout, b"\n");
1083 std::process::exit(2);
1084 }
1085 }
1086
1087 Cli::try_parse().unwrap_or_else(|e| {
1088 if matches!(
1089 e.kind(),
1090 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
1091 ) {
1092 let mut stdout = std::io::stdout();
1093 let _ = std::io::Write::write_all(&mut stdout, e.to_string().as_bytes());
1094 std::process::exit(0);
1095 }
1096
1097 let mut stdout = std::io::stdout();
1098 let message = render_cli_json(&cli_error_value(
1099 &e.to_string(),
1100 "run hypha --help to inspect all commands and flags",
1101 ));
1102 let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1103 let _ = std::io::Write::write_all(&mut stdout, b"\n");
1104 std::process::exit(2);
1105 })
1106}