Skip to main content

hypha/cli/
mod.rs

1//! Single source of truth for Hypha's CLI shape and user-facing help text.
2//!
3//! Keep command descriptions, examples, and argument docs here so both runtime
4//! help and generated reference docs stay aligned.
5
6use 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    "  {\"code\": \"ok\", \"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    /// Output format: json (default), yaml, plain; help also accepts markdown
36    #[arg(short, long, default_value = "json", global = true)]
37    pub output: String,
38
39    /// Log categories (comma-separated): startup, request, ...
40    #[arg(long, value_delimiter = ',', global = true)]
41    pub log: Vec<String>,
42
43    #[command(subcommand)]
44    pub command: Commands,
45}
46
47#[derive(Subcommand, Serialize)]
48#[serde(tag = "command", rename_all = "snake_case")]
49pub enum Commands {
50    // ═══════════════════════════════════════════
51    // Spore Lifecycle Commands (top-level)
52    // ═══════════════════════════════════════════
53    /// Resolve a CMN URI and show metadata without downloading
54    #[command(after_long_help = concat!(
55        "URI types:\n",
56        "  cmn://DOMAIN                       List all spores on a site\n",
57        "  cmn://DOMAIN/HASH                  View a specific spore\n",
58        "  cmn://DOMAIN --id SPORE_ID         View latest spore with id from a site\n",
59        "\n",
60        "Examples:\n",
61        "  hypha sense cmn://cmn.dev\n",
62        "  hypha sense cmn://cmn.dev --id cmn-spec\n",
63        "  hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
64        "  hypha sense cmn://cmn.dev -o yaml",
65    ))]
66    Sense {
67        /// CMN URI (cmn://DOMAIN or cmn://DOMAIN/HASH)
68        uri: String,
69        /// Spore id to resolve from the domain's latest mycelium inventory
70        #[arg(long)]
71        id: Option<String>,
72    },
73
74    /// Evaluate spore: download for review, or record a verdict
75    #[command(after_long_help = concat!(
76        "Without --verdict: downloads the spore for local review.\n",
77        "With --verdict: records a verdict (sweet, fresh, safe, rotten, toxic).\n",
78        "\n",
79        "Examples:\n",
80        "  hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
81        "  hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
82            " --verdict safe --notes \"Reviewed: clean code\"\n",
83        "  hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
84            " --verdict safe --domain cmn.dev --synapse https://synapse.cmn.dev",
85    ))]
86    Taste {
87        /// CMN URI (e.g., cmn://cmn.dev/HASH)
88        uri: String,
89        /// Record verdict: sweet, fresh, safe, rotten, or toxic
90        #[arg(long, value_name = "VERDICT")]
91        verdict: Option<substrate::TasteVerdict>,
92        /// Notes about the verdict
93        #[arg(long)]
94        notes: Option<String>,
95        /// Synapse URL to pull/share taste reports
96        #[arg(long)]
97        synapse: Option<String>,
98        /// Auth token for synapse (overrides configured token)
99        #[arg(long)]
100        synapse_token_secret: Option<String>,
101        /// Domain to sign report with (requires --synapse and --verdict)
102        #[arg(long)]
103        domain: Option<String>,
104    },
105
106    /// Create a working copy of a spore (auto-detects best distribution format)
107    #[command(after_long_help = concat!(
108        "Distribution sources (auto-detected):\n",
109        "  archive    Download .tar.zst archive (default, fastest)\n",
110        "  git        Clone from dist.git URL if available\n",
111        "\n",
112        "Examples:\n",
113        "  hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
114        "  hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
115            " my-project --vcs git\n",
116        "  hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
117            " --dist git",
118    ))]
119    Spawn {
120        /// CMN URI (e.g., cmn://cmn.dev/HASH)
121        uri: String,
122        /// Target directory (default: ./<spore-id>)
123        directory: Option<String>,
124        /// Initialize version control after spawn (e.g., --vcs git)
125        #[arg(long, value_enum, value_name = "TYPE")]
126        vcs: Option<VcsArg>,
127        /// Preferred distribution source: archive (default) or git
128        #[arg(long, value_enum, value_name = "SOURCE")]
129        dist: Option<DistArg>,
130        /// Fetch bonds after spawn
131        #[arg(long)]
132        bond: bool,
133    },
134
135    /// Pull latest changes from spawn source via Synapse lineage
136    #[command(after_long_help = "\
137Run inside a previously spawned directory to update it.
138Uses Synapse lineage to discover newer versions from the same publisher.
139
140If local files have been modified (git dirty or tree hash mismatch),
141grow refuses to overwrite them and shows the cache path for manual merge.
142
143Examples:
144  hypha grow
145  hypha grow --synapse synapse.cmn.dev
146  hypha grow --dist git
147  hypha grow --dist archive
148  hypha grow --bond --synapse synapse.cmn.dev")]
149    Grow {
150        /// Override distribution source: archive or git
151        #[arg(long, value_enum, value_name = "SOURCE")]
152        dist: Option<DistArg>,
153        /// Synapse to query for updates (domain or URL)
154        #[arg(long)]
155        synapse: Option<String>,
156        /// Auth token for synapse (overrides configured token)
157        #[arg(long)]
158        synapse_token_secret: Option<String>,
159        /// Also check depends_on/follows/extends bonds for updates via Synapse lineage, and fetch all bonds to .cmn/bonds/
160        #[arg(long)]
161        bond: bool,
162    },
163
164    /// Prepare spores for AI-assisted merge
165    #[command(after_long_help = concat!(
166        "Absorb downloads spores into .cmn/absorb/ for AI-assisted merge.\n",
167        "Use --discover to auto-discover descendants via Synapse.\n",
168        "\n",
169        "Examples:\n",
170        "  hypha absorb cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
171        "  hypha absorb --discover --synapse https://synapse.cmn.dev\n",
172        "  hypha absorb --discover --synapse https://synapse.cmn.dev --max-depth 5",
173    ))]
174    Absorb {
175        /// CMN URIs to absorb (e.g., cmn://cmn.dev/HASH)
176        #[arg(required_unless_present = "discover")]
177        uris: Vec<String>,
178        /// Auto-discover descendants from current spore's spawned_from bonds
179        #[arg(long)]
180        discover: bool,
181        /// Synapse server URL (required with --discover)
182        #[arg(long)]
183        synapse: Option<String>,
184        /// Auth token for synapse (overrides configured token)
185        #[arg(long)]
186        synapse_token_secret: Option<String>,
187        /// Maximum depth for lineage discovery (default: 10)
188        #[arg(long, default_value = "10")]
189        max_depth: u32,
190    },
191
192    /// Fetch all bonds from spore.core.json to .cmn/bonds/
193    #[command(after_long_help = "\
194Examples:
195  hypha bond
196  hypha bond --status
197  hypha bond --clean")]
198    Bond {
199        /// Clean orphaned bonds not in spore.core.json
200        #[arg(long)]
201        clean: bool,
202        /// Show bond status without fetching
203        #[arg(long)]
204        status: bool,
205    },
206
207    /// Copy a spore to your domain (same hash, re-signed capsule)
208    #[command(after_long_help = "\
209Replicates spores from another domain to yours. The hash stays the same
210because core + core_signature are preserved. Only capsule_signature changes.
211
212Examples:
213  hypha replicate cmn://other.dev/HASH --domain my.dev
214  hypha replicate --refs --domain my.dev")]
215    Replicate {
216        /// CMN URI(s) to replicate
217        #[arg(required_unless_present = "refs")]
218        uris: Vec<String>,
219        /// Replicate all non-self bonds from spore.core.json
220        #[arg(long)]
221        refs: bool,
222        /// Target domain (required)
223        #[arg(long)]
224        domain: String,
225        /// Custom site directory
226        #[arg(long)]
227        site_path: Option<String>,
228    },
229
230    /// Create or update spore.core.json in working directory
231    #[command(after_long_help = "\
232Examples:
233  hypha hatch --id my-tool --name \"My Tool\" --synopsis \"A useful tool\"
234  hypha hatch --intent \"Provide a reusable HTTP client for CMN agents\" --mutations \"Initial release\"
235  hypha hatch --license MIT --domain cmn.dev
236
237Subcommands:
238  hypha hatch bond set/remove/clear   Manage bonds in spore.core.json
239  hypha hatch tree set/show            Manage tree configuration")]
240    #[command(args_conflicts_with_subcommands = true)]
241    Hatch {
242        /// Opaque identifier stored in spore.core.json
243        #[arg(long)]
244        id: Option<String>,
245        /// Version string (e.g., 1.0.0)
246        #[arg(long)]
247        version: Option<String>,
248        /// Display name
249        #[arg(long)]
250        name: Option<String>,
251        /// Publisher domain
252        #[arg(long)]
253        domain: Option<String>,
254        /// Short description
255        #[arg(long)]
256        synopsis: Option<String>,
257        /// Why this spore exists — permanent across releases (repeatable)
258        #[arg(long)]
259        intent: Vec<String>,
260        /// What changed relative to the spawned-from parent (repeatable)
261        #[arg(long)]
262        mutations: Vec<String>,
263        /// License (SPDX identifier)
264        #[arg(long)]
265        license: Option<String>,
266
267        #[command(subcommand)]
268        #[serde(skip)]
269        command: Option<HatchCommands>,
270    },
271
272    /// Sign and publish spore to mycelium site
273    #[command(after_long_help = "\
274Requires `hypha mycelium root` first to set up the site.
275
276Examples:
277  hypha release --domain cmn.dev
278  hypha release --domain cmn.dev --source ./my-spore
279  hypha release --domain cmn.dev --dry-run          # pre-compute URI without releasing
280  hypha release --domain cmn.dev --archive zstd
281  hypha release --domain cmn.dev --dist-git https://github.com/user/repo --dist-ref v1.0")]
282    Release {
283        /// Target domain (required)
284        #[arg(long)]
285        domain: String,
286        /// Spore source directory (default: current directory)
287        #[arg(long)]
288        source: Option<String>,
289        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
290        #[arg(long)]
291        site_path: Option<String>,
292        /// External git repository URL
293        #[arg(long)]
294        dist_git: Option<String>,
295        /// Git ref: tag/branch/commit (requires --dist-git)
296        #[arg(long)]
297        dist_ref: Option<String>,
298        /// Archive format for release generation (currently only: zstd)
299        #[arg(long, value_name = "FORMAT", default_value = "zstd")]
300        archive: String,
301        /// Pre-compute URI without writing any files
302        #[arg(long)]
303        dry_run: bool,
304    },
305
306    // ═══════════════════════════════════════════
307    // Discovery Commands
308    // ═══════════════════════════════════════════
309    /// Trace spore lineage: descendants (in, default) or ancestors (out)
310    #[command(after_long_help = concat!(
311        "Direction:\n",
312        "  --direction in   Find descendants / forks (default)\n",
313        "  --direction out  Trace ancestors / spawn chain\n",
314        "\n",
315        "Examples:\n",
316        "  hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
317            " --synapse https://synapse.cmn.dev\n",
318        "  hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
319            " --direction out --synapse https://synapse.cmn.dev\n",
320        "  hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
321            " --synapse https://synapse.cmn.dev --max-depth 5",
322    ))]
323    Lineage {
324        /// CMN URI (e.g., cmn://cmn.dev/HASH)
325        uri: String,
326        /// Direction: in (descendants, default) or out (ancestors)
327        #[arg(long, value_enum)]
328        direction: Option<DirectionArg>,
329        /// Synapse server (domain or URL, default: configured default)
330        #[arg(long)]
331        synapse: Option<String>,
332        /// Auth token for synapse (overrides configured token)
333        #[arg(long)]
334        synapse_token_secret: Option<String>,
335        /// Maximum traversal depth (default: 10)
336        #[arg(long, default_value = "10")]
337        max_depth: u32,
338    },
339
340    /// Search for spores by keyword (semantic search via Synapse)
341    #[command(after_long_help = "\
342Examples:
343  hypha search \"protocol spec\" --synapse https://synapse.cmn.dev
344  hypha search \"data format\" --synapse https://synapse.cmn.dev --domain cmn.dev
345  hypha search \"agent tools\" --synapse https://synapse.cmn.dev --license MIT --limit 5
346  hypha search \"http client\" --bonds spawned_from:cmn://cmn.dev/b3.abc123")]
347    Search {
348        /// Search query text
349        query: String,
350        /// Synapse server (domain or URL, default: configured default)
351        #[arg(long)]
352        synapse: Option<String>,
353        /// Auth token for synapse (overrides configured token)
354        #[arg(long)]
355        synapse_token_secret: Option<String>,
356        /// Filter by domain
357        #[arg(long)]
358        domain: Option<String>,
359        /// Filter by license (SPDX identifier)
360        #[arg(long)]
361        license: Option<String>,
362        /// Filter by bond relationship (format: relation:uri, comma-separated for AND)
363        #[arg(long)]
364        bonds: Option<String>,
365        /// Maximum results (default: 20)
366        #[arg(long, default_value = "20")]
367        limit: u32,
368    },
369
370    // ═══════════════════════════════════════════
371    // Infrastructure Commands
372    // ═══════════════════════════════════════════
373    /// Manage local mycelium site
374    Mycelium {
375        #[command(subcommand)]
376        #[serde(flatten)]
377        action: MyceliumAction,
378    },
379
380    /// Manage Synapse node connections
381    Synapse {
382        #[command(subcommand)]
383        #[serde(flatten)]
384        action: SynapseAction,
385    },
386
387    /// Manage local cache
388    Cache {
389        #[command(subcommand)]
390        #[serde(flatten)]
391        action: CacheAction,
392    },
393
394    /// View or modify hypha configuration
395    Config {
396        #[command(subcommand)]
397        #[serde(flatten)]
398        action: ConfigAction,
399    },
400
401    /// Install, uninstall, or inspect the bundled Hypha agent skill
402    #[command(after_long_help = "\
403Examples:
404  hypha skill status
405  hypha skill install --agent codex
406  hypha skill install --agent claude-code --scope project
407  hypha skill uninstall --agent opencode --skills-dir /tmp/skills --force")]
408    Skill {
409        #[command(subcommand)]
410        #[serde(flatten)]
411        action: SkillCommand,
412    },
413}
414
415#[derive(Subcommand, Serialize)]
416#[serde(tag = "action", rename_all = "snake_case")]
417pub enum SkillCommand {
418    /// Report whether the bundled Hypha skill is installed and current
419    Status(SkillOptionsArg),
420    /// Install or refresh the bundled Hypha skill
421    Install(SkillOptionsArg),
422    /// Remove the bundled Hypha skill
423    Uninstall(SkillOptionsArg),
424}
425
426#[derive(Args, Serialize)]
427pub struct SkillOptionsArg {
428    /// Agent target to manage
429    #[arg(long, value_enum, default_value = "all")]
430    pub agent: SkillAgentArg,
431    /// Install scope
432    #[arg(long, value_enum, default_value = "personal")]
433    pub scope: SkillScopeArg,
434    /// Explicit skills directory; requires a single --agent
435    #[arg(long)]
436    pub skills_dir: Option<String>,
437    /// Overwrite or remove an unmanaged skill at the target path
438    #[arg(long)]
439    pub force: bool,
440}
441
442#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
443#[serde(rename_all = "kebab-case")]
444pub enum SkillAgentArg {
445    All,
446    Codex,
447    ClaudeCode,
448    Opencode,
449}
450
451#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
452#[serde(rename_all = "lowercase")]
453pub enum SkillScopeArg {
454    Personal,
455    Project,
456}
457
458/// Preferred distribution source for spawn/grow.
459#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
460#[serde(rename_all = "lowercase")]
461pub enum DistArg {
462    Archive,
463    Git,
464}
465
466impl DistArg {
467    /// Canonical string passed to the library layer.
468    pub fn as_str(self) -> &'static str {
469        match self {
470            Self::Archive => "archive",
471            Self::Git => "git",
472        }
473    }
474}
475
476/// Version-control initialization choice for spawn.
477#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
478#[serde(rename_all = "lowercase")]
479pub enum VcsArg {
480    Git,
481    None,
482}
483
484impl VcsArg {
485    /// Canonical string passed to the library layer.
486    pub fn as_str(self) -> &'static str {
487        match self {
488            Self::Git => "git",
489            Self::None => "none",
490        }
491    }
492}
493
494/// Lineage traversal direction.
495#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
496#[serde(rename_all = "lowercase")]
497pub enum DirectionArg {
498    In,
499    Out,
500}
501
502impl DirectionArg {
503    /// Canonical string passed to the library layer.
504    pub fn as_str(self) -> &'static str {
505        match self {
506            Self::In => "in",
507            Self::Out => "out",
508        }
509    }
510}
511
512#[derive(Subcommand, Serialize)]
513#[serde(tag = "action", rename_all = "snake_case")]
514pub enum HatchCommands {
515    /// Manage bonds in spore.core.json
516    #[command(after_long_help = "\
517Examples:
518  hypha hatch bond set --uri cmn://cmn.dev/b3.abc --relation follows --id my-lib --reason \"Core library\"
519  hypha hatch bond set --uri cmn://cmn.dev/b3.abc --with 'mints=[\"https://mint.example.com\"]'
520  hypha hatch bond remove --relation follows
521  hypha hatch bond clear")]
522    Bond {
523        #[command(subcommand)]
524        #[serde(flatten)]
525        command: HatchBondCommands,
526    },
527    /// Manage tree configuration in spore.core.json
528    Tree {
529        #[command(subcommand)]
530        #[serde(flatten)]
531        command: HatchTreeCommands,
532    },
533}
534
535#[derive(Subcommand, Serialize)]
536#[serde(tag = "action", rename_all = "snake_case")]
537pub enum HatchBondCommands {
538    /// Add or update a bond (upsert by URI)
539    Set {
540        /// Bond URI (match key)
541        #[arg(long)]
542        uri: String,
543        /// Bond relation (required for new bonds)
544        #[arg(long)]
545        relation: Option<substrate::BondRelation>,
546        /// Bond id
547        #[arg(long)]
548        id: Option<String>,
549        /// Bond reason
550        #[arg(long)]
551        reason: Option<String>,
552        /// Bond parameters (KEY=VALUE, value is parsed as JSON; repeatable)
553        #[arg(long = "with", value_name = "KEY=VALUE")]
554        with_entries: Vec<String>,
555    },
556    /// Remove bonds by URI and/or relation
557    Remove {
558        /// Remove bonds matching this URI
559        #[arg(long)]
560        uri: Option<String>,
561        /// Remove bonds matching this relation
562        #[arg(long)]
563        relation: Option<substrate::BondRelation>,
564    },
565    /// Remove all bonds
566    Clear,
567}
568
569#[derive(Subcommand, Serialize)]
570#[serde(tag = "action", rename_all = "snake_case")]
571pub enum HatchTreeCommands {
572    /// Set tree configuration fields
573    Set {
574        /// Hash algorithm (e.g., blob_tree_blake3_nfc)
575        #[arg(long)]
576        algorithm: Option<String>,
577        /// File/directory names to exclude from hashing (repeatable)
578        #[arg(long, num_args = 1..)]
579        exclude_names: Option<Vec<String>>,
580        /// Ignore-rule files to follow (repeatable)
581        #[arg(long, num_args = 1..)]
582        follow_rules: Option<Vec<String>>,
583    },
584    /// Show current tree configuration
585    Show,
586}
587
588#[derive(Subcommand, Serialize)]
589#[serde(tag = "action", rename_all = "snake_case")]
590pub enum MyceliumAction {
591    /// Establish a new site for a domain (or update existing)
592    #[command(after_long_help = "\
593Creates ~/.cmn/mycelium/<domain>/ with key pair and site structure.
594Run this once before `hypha release`.
595
596With --hub, creates a taste-only account on a hosted hub (e.g. cmnhub.com):
597  1. Generates ed25519 key pair
598  2. Computes subdomain from pubkey (ed-<base32>.hub)
599  3. Creates taste-only cmn.json with taste endpoint
600  4. Registers hub as a synapse node
601  5. Sets [defaults.taste] so `hypha taste` auto-submits
602
603After --hub, register with the hub then taste without extra flags:
604  curl -X POST https://cmnhub.com/synapse/pulse -H 'Content-Type: application/json' \\
605    -d @~/.cmn/mycelium/ed-xxx.cmnhub.com/public/.well-known/cmn.json
606  hypha taste cmn://example.com/b3.HASH --verdict safe
607
608Examples:
609  hypha mycelium root cmn.dev --name \"CMN\" --synopsis \"Code Mycelial Network\"
610  hypha mycelium root cmn.dev --endpoints-base https://cmn.dev
611  hypha mycelium root example.com --site-path /custom/path
612  hypha mycelium root --hub cmnhub.com")]
613    Root {
614        /// Domain name (auto-computed when --hub is used)
615        domain: Option<String>,
616        /// Hub domain (e.g., cmnhub.com). Generates a key, computes subdomain
617        /// from pubkey (ed-<base32>), and sets domain + endpoints automatically.
618        #[arg(long, conflicts_with = "endpoints_base")]
619        hub: Option<String>,
620        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
621        #[arg(long)]
622        site_path: Option<String>,
623        /// Site or author name
624        #[arg(long)]
625        name: Option<String>,
626        /// Brief description of the site or author
627        #[arg(long)]
628        synopsis: Option<String>,
629        /// Bio (markdown)
630        #[arg(long)]
631        bio: Option<String>,
632        /// Base URL for endpoints (e.g., https://example.com)
633        #[arg(long)]
634        endpoints_base: Option<String>,
635    },
636    /// Show site status
637    #[command(after_long_help = "\
638Examples:
639  hypha mycelium status
640  hypha mycelium status cmn.dev
641  hypha mycelium status cmn.dev --id cmn-spec --site-path deploy/cmn.dev")]
642    Status {
643        /// Domain name (optional, lists all if not specified)
644        domain: Option<String>,
645        /// Custom site directory
646        #[arg(long)]
647        site_path: Option<String>,
648        /// Spore id to resolve from the local mycelium inventory
649        #[arg(long)]
650        id: Option<String>,
651    },
652    /// Start a local HTTP server to serve the site (for debugging)
653    #[command(after_long_help = "\
654Examples:
655  hypha mycelium serve
656  hypha mycelium serve cmn.dev --port 3000")]
657    Serve {
658        /// Domain name
659        domain: Option<String>,
660        /// Custom site directory
661        #[arg(long)]
662        site_path: Option<String>,
663        /// Port to listen on (default: 8080)
664        #[arg(long, default_value = "8080")]
665        port: u16,
666    },
667    /// Manage nutrient methods (add/remove/clear)
668    #[command(after_long_help = "\
669Examples:
670  hypha mycelium nutrient add cmn.dev --type lightning_address --with address=user@example.com
671  hypha mycelium nutrient add cmn.dev --type url --with url=https://example.com --with label=Donate
672  hypha mycelium nutrient remove cmn.dev --type url
673  hypha mycelium nutrient clear cmn.dev")]
674    Nutrient {
675        #[command(subcommand)]
676        #[serde(flatten)]
677        command: NutrientCommands,
678    },
679    /// Send a pulse to a synapse indexer
680    #[command(after_long_help = "\
681Examples:
682  hypha mycelium pulse --synapse synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json
683  hypha mycelium pulse --synapse https://synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json")]
684    Pulse {
685        /// Synapse server (domain or URL, default: configured default)
686        #[arg(long)]
687        synapse: Option<String>,
688        /// Auth token for synapse (overrides configured token)
689        #[arg(long)]
690        synapse_token_secret: Option<String>,
691        /// Path to signed mycelium.json
692        #[arg(long)]
693        file: String,
694    },
695}
696
697#[derive(Subcommand, Serialize)]
698#[serde(tag = "action", rename_all = "snake_case")]
699pub enum NutrientCommands {
700    /// Add or update a nutrient method (upsert by type)
701    Add {
702        /// Domain name
703        domain: String,
704        /// Nutrient method type (e.g., lightning_address, url, evm, solana)
705        #[arg(long = "type", value_name = "TYPE")]
706        method_type: String,
707        /// Nutrient parameters (KEY=VALUE, value is parsed as JSON; repeatable)
708        #[arg(long = "with", value_name = "KEY=VALUE")]
709        with_entries: Vec<String>,
710        /// Custom site directory
711        #[arg(long)]
712        site_path: Option<String>,
713    },
714    /// Remove a nutrient method by type
715    Remove {
716        /// Domain name
717        domain: String,
718        /// Nutrient method type to remove
719        #[arg(long = "type", value_name = "TYPE")]
720        method_type: String,
721        /// Custom site directory
722        #[arg(long)]
723        site_path: Option<String>,
724    },
725    /// Remove all nutrient methods
726    Clear {
727        /// Domain name
728        domain: String,
729        /// Custom site directory
730        #[arg(long)]
731        site_path: Option<String>,
732    },
733}
734
735#[derive(Subcommand, Serialize)]
736#[serde(tag = "action", rename_all = "snake_case")]
737pub enum SynapseAction {
738    /// Discover Synapse instances via the network
739    #[command(after_long_help = "\
740Examples:
741  hypha synapse discover
742  hypha synapse discover --synapse https://synapse.cmn.dev")]
743    Discover {
744        /// Synapse to query (domain or URL, default: configured default)
745        #[arg(long)]
746        synapse: Option<String>,
747        /// Auth token for synapse (overrides configured token)
748        #[arg(long)]
749        synapse_token_secret: Option<String>,
750    },
751    /// List configured Synapse nodes
752    #[command(after_long_help = "\
753Examples:
754  hypha synapse list")]
755    List,
756    /// Check health of a Synapse instance
757    #[command(after_long_help = "\
758Examples:
759  hypha synapse health
760  hypha synapse health synapse.cmn.dev
761  hypha synapse health https://synapse.cmn.dev")]
762    Health {
763        /// Synapse domain or URL (default: configured default)
764        synapse: Option<String>,
765        /// Auth token for synapse (overrides configured token)
766        #[arg(long)]
767        synapse_token_secret: Option<String>,
768    },
769    /// Add a Synapse node
770    #[command(after_long_help = "\
771Examples:
772  hypha synapse add https://synapse.cmn.dev")]
773    Add {
774        /// Synapse URL
775        url: String,
776    },
777    /// Remove a Synapse node
778    #[command(after_long_help = "\
779Examples:
780  hypha synapse remove synapse.cmn.dev")]
781    Remove {
782        /// Synapse domain
783        domain: String,
784    },
785    /// Set default Synapse node
786    #[command(after_long_help = "\
787Examples:
788  hypha synapse use synapse.cmn.dev")]
789    Use {
790        /// Synapse domain
791        domain: String,
792    },
793    /// Configure a Synapse node (token, etc.)
794    #[command(after_long_help = "\
795Examples:
796  hypha synapse config synapse.cmn.dev --token-secret sk-abc123
797  hypha synapse config synapse.cmn.dev --token-secret \"\"    # clear token")]
798    Config {
799        /// Synapse domain
800        domain: String,
801        /// Auth token (use empty string to clear)
802        #[arg(long)]
803        token_secret: Option<String>,
804    },
805}
806
807#[derive(Subcommand, Serialize)]
808#[serde(tag = "action", rename_all = "snake_case")]
809pub enum CacheAction {
810    /// List all cached spores
811    #[command(after_long_help = "\
812Examples:
813  hypha cache list
814  hypha cache list -o yaml")]
815    List,
816    /// Remove old or all cached items
817    #[command(after_long_help = "\
818Examples:
819  hypha cache clean --all")]
820    Clean {
821        /// Remove all cached items
822        #[arg(long)]
823        all: bool,
824    },
825    /// Show local filesystem path for a cached spore
826    #[command(after_long_help = concat!(
827        "Examples:\n",
828        "  hypha cache path cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
829    ))]
830    Path {
831        /// CMN URI (e.g., cmn://cmn.dev/HASH)
832        uri: String,
833    },
834}
835
836#[derive(Subcommand, Serialize)]
837#[serde(tag = "action", rename_all = "snake_case")]
838pub enum ConfigAction {
839    /// Show current configuration (merged defaults + config.toml)
840    #[command(after_long_help = "\
841Examples:
842  hypha config list
843  hypha config list -o yaml")]
844    List,
845    /// Set a configuration value
846    #[command(after_long_help = "\
847Dotted keys map to TOML sections:
848  cache.path            Custom cache directory
849  cache.cmn_ttl_s       cmn.json cache TTL in seconds
850  cache.key_trust_ttl_s Key trust cache TTL in seconds
851  cache.key_trust_refresh_mode
852                        Key trust refresh mode: expired | always | offline
853  cache.key_trust_synapse_witness_mode
854                        Key trust fallback when domain is offline: allow | require_domain
855  cache.spore_max_download_bytes
856                        Max spore archive download bytes
857  cache.spore_max_extract_bytes
858                        Max total bytes extracted from a spore archive
859  cache.spore_max_extract_files
860                        Max files extracted from a spore archive
861  cache.spore_max_extract_file_bytes
862                        Max bytes extracted for one spore archive file
863  cache.spore_reject_path_components
864                        TOML string array of protected received path components
865  cache.clock_skew_tolerance_s
866                        Clock skew tolerance in seconds for key trust TTL (default: 300)
867  defaults.synapse      Default synapse domain
868  defaults.domain       Default domain for publishing (release)
869  defaults.taste.synapse
870                        Synapse to submit taste reports to (overrides defaults.synapse for taste)
871  defaults.taste.domain Domain to sign taste reports with (overrides defaults.domain for taste)
872
873Examples:
874  hypha config set cache.cmn_ttl_s 600
875  hypha config set cache.key_trust_ttl_s 604800
876  hypha config set cache.key_trust_refresh_mode offline
877  hypha config set cache.key_trust_synapse_witness_mode require_domain
878  hypha config set cache.spore_max_download_bytes 1073741824
879  hypha config set cache.spore_reject_path_components '[\".git\", \".cmn\"]'
880  hypha config set cache.path /tmp/hypha-cache
881  hypha config set defaults.synapse synapse.cmn.dev
882  hypha config set defaults.taste.synapse cmnhub.com
883  hypha config set defaults.taste.domain ed-xxx.cmnhub.com")]
884    Set {
885        /// Config key (dotted path, e.g. cache.cmn_ttl_s)
886        key: String,
887        /// Value to set
888        value: String,
889    },
890}
891
892/// Build hypha's CLI argument-error envelope.
893///
894/// Keeps a stable machine-readable shape for argument/parse failures
895/// (`error_code`, `retryable`) on top of the agent-first-data error builder,
896/// which no longer emits those fields itself.
897pub(crate) fn cli_error_value(message: &str, hint: &str) -> serde_json::Value {
898    let mut value = agent_first_data::build_json_error(
899        message,
900        Some(hint),
901        Some(serde_json::json!({ "duration_ms": 0 })),
902    );
903    if let serde_json::Value::Object(map) = &mut value {
904        map.insert(
905            "error_code".to_string(),
906            serde_json::Value::String("invalid_request".to_string()),
907        );
908        map.insert("retryable".to_string(), serde_json::Value::Bool(false));
909    }
910    value
911}
912
913pub fn parse_or_exit() -> Cli {
914    let raw: Vec<String> = std::env::args().collect();
915
916    match agent_first_data::cli_handle_help_or_continue(
917        &raw,
918        &Cli::command(),
919        &agent_first_data::HelpConfig::human_cli_default(),
920    ) {
921        Ok(Some(help)) => {
922            let mut stdout = std::io::stdout();
923            let _ = std::io::Write::write_all(&mut stdout, help.as_bytes());
924            std::process::exit(0);
925        }
926        Ok(None) => {}
927        Err(err) => {
928            let mut stdout = std::io::stdout();
929            let message = agent_first_data::output_json(&err);
930            let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
931            let _ = std::io::Write::write_all(&mut stdout, b"\n");
932            std::process::exit(2);
933        }
934    }
935
936    Cli::try_parse().unwrap_or_else(|e| {
937        if matches!(e.kind(), clap::error::ErrorKind::DisplayVersion) {
938            let mut stdout = std::io::stdout();
939            let message = agent_first_data::output_json(&agent_first_data::build_json_ok(
940                serde_json::json!({ "version": env!("CARGO_PKG_VERSION") }),
941                None,
942            ));
943            let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
944            let _ = std::io::Write::write_all(&mut stdout, b"\n");
945            std::process::exit(0);
946        }
947        if matches!(e.kind(), clap::error::ErrorKind::DisplayHelp) {
948            let mut stdout = std::io::stdout();
949            let _ = std::io::Write::write_all(&mut stdout, e.to_string().as_bytes());
950            std::process::exit(0);
951        }
952
953        let mut stdout = std::io::stdout();
954        let message = agent_first_data::output_json(&cli_error_value(
955            &e.to_string(),
956            "run hypha --help to inspect all commands and flags",
957        ));
958        let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
959        let _ = std::io::Write::write_all(&mut stdout, b"\n");
960        std::process::exit(2);
961    })
962}