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