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    "  {\"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    /// 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    /// Redirect stdout to this file
44    #[arg(long, value_name = "PATH", global = true)]
45    pub stdout_file: Option<std::path::PathBuf>,
46
47    /// Redirect stderr to this file
48    #[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    // ═══════════════════════════════════════════
59    // Spore Lifecycle Commands (top-level)
60    // ═══════════════════════════════════════════
61    /// Resolve a CMN URI and show metadata without downloading
62    #[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        /// CMN URI (cmn://DOMAIN or cmn://DOMAIN/HASH)
76        uri: String,
77        /// Spore id to resolve from the domain's latest mycelium inventory
78        #[arg(long)]
79        id: Option<String>,
80    },
81
82    /// Evaluate spore: download for review, or record a verdict
83    #[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        /// CMN URI (e.g., cmn://cmn.dev/HASH)
96        uri: String,
97        /// Record verdict: sweet, fresh, safe, rotten, or toxic
98        #[arg(long, value_name = "VERDICT")]
99        verdict: Option<substrate::TasteVerdict>,
100        /// Notes about the verdict
101        #[arg(long)]
102        notes: Option<String>,
103        /// Synapse URL to pull/share taste reports
104        #[arg(long)]
105        synapse: Option<String>,
106        /// Auth token for synapse (overrides configured token)
107        #[arg(long)]
108        synapse_token_secret: Option<String>,
109        /// Domain to sign report with (requires --synapse and --verdict)
110        #[arg(long)]
111        domain: Option<String>,
112    },
113
114    /// Create a working copy of a spore (auto-detects best distribution format)
115    #[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        /// CMN URI (e.g., cmn://cmn.dev/HASH)
129        uri: String,
130        /// Target directory (default: ./<spore-id>)
131        directory: Option<String>,
132        /// Initialize version control after spawn (e.g., --vcs git)
133        #[arg(long, value_enum, value_name = "TYPE")]
134        vcs: Option<VcsArg>,
135        /// Preferred distribution source: archive (default) or git
136        #[arg(long, value_enum, value_name = "SOURCE")]
137        dist: Option<DistArg>,
138        /// Fetch bonds after spawn
139        #[arg(long)]
140        bond: bool,
141    },
142
143    /// Pull latest changes from spawn source via Synapse lineage
144    #[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        /// Override distribution source: archive or git
159        #[arg(long, value_enum, value_name = "SOURCE")]
160        dist: Option<DistArg>,
161        /// Synapse to query for updates (domain or URL)
162        #[arg(long)]
163        synapse: Option<String>,
164        /// Auth token for synapse (overrides configured token)
165        #[arg(long)]
166        synapse_token_secret: Option<String>,
167        /// Also check depends_on/follows/extends bonds for updates via Synapse lineage, and fetch all bonds to .cmn/bonds/
168        #[arg(long)]
169        bond: bool,
170    },
171
172    /// Prepare spores for AI-assisted merge
173    #[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        /// CMN URIs to absorb (e.g., cmn://cmn.dev/HASH)
184        #[arg(required_unless_present = "discover")]
185        uris: Vec<String>,
186        /// Auto-discover descendants from current spore's spawned_from bonds
187        #[arg(long)]
188        discover: bool,
189        /// Synapse server URL (required with --discover)
190        #[arg(long)]
191        synapse: Option<String>,
192        /// Auth token for synapse (overrides configured token)
193        #[arg(long)]
194        synapse_token_secret: Option<String>,
195        /// Maximum depth for lineage discovery (default: 10)
196        #[arg(long, default_value = "10")]
197        max_depth: u32,
198    },
199
200    /// Fetch all bonds from spore.core.json to .cmn/bonds/
201    #[command(after_long_help = "\
202Examples:
203  hypha bond
204  hypha bond --status
205  hypha bond --clean")]
206    Bond {
207        /// Clean orphaned bonds not in spore.core.json
208        #[arg(long)]
209        clean: bool,
210        /// Show bond status without fetching
211        #[arg(long)]
212        status: bool,
213    },
214
215    /// Copy a spore to your domain (same hash, re-signed capsule)
216    #[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        /// CMN URI(s) to replicate
225        #[arg(required_unless_present = "refs")]
226        uris: Vec<String>,
227        /// Replicate all non-self bonds from spore.core.json
228        #[arg(long)]
229        refs: bool,
230        /// Target domain (required)
231        #[arg(long)]
232        domain: String,
233        /// Custom site directory
234        #[arg(long)]
235        site_path: Option<String>,
236    },
237
238    /// Create or update spore.core.json in working directory
239    #[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        /// Opaque identifier stored in spore.core.json
251        #[arg(long)]
252        id: Option<String>,
253        /// Version string (e.g., 1.0.0)
254        #[arg(long)]
255        version: Option<String>,
256        /// Display name
257        #[arg(long)]
258        name: Option<String>,
259        /// Publisher domain
260        #[arg(long)]
261        domain: Option<String>,
262        /// Short description
263        #[arg(long)]
264        synopsis: Option<String>,
265        /// Why this spore exists — permanent across releases (repeatable)
266        #[arg(long)]
267        intent: Vec<String>,
268        /// What changed relative to the spawned-from parent (repeatable)
269        #[arg(long)]
270        mutations: Vec<String>,
271        /// License (SPDX identifier)
272        #[arg(long)]
273        license: Option<String>,
274
275        #[command(subcommand)]
276        #[serde(skip)]
277        command: Option<HatchCommands>,
278    },
279
280    /// Sign and publish spore to mycelium site
281    #[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        /// Target domain (required)
292        #[arg(long)]
293        domain: String,
294        /// Spore source directory (default: current directory)
295        #[arg(long)]
296        source: Option<String>,
297        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
298        #[arg(long)]
299        site_path: Option<String>,
300        /// External git repository URL
301        #[arg(long)]
302        dist_git: Option<String>,
303        /// Git ref: tag/branch/commit (requires --dist-git)
304        #[arg(long)]
305        dist_ref: Option<String>,
306        /// Archive format for release generation (currently only: zstd)
307        #[arg(long, value_name = "FORMAT", default_value = "zstd")]
308        archive: String,
309        /// Pre-compute URI without writing any files
310        #[arg(long)]
311        dry_run: bool,
312    },
313
314    // ═══════════════════════════════════════════
315    // Discovery Commands
316    // ═══════════════════════════════════════════
317    /// Trace spore lineage: descendants (in, default) or ancestors (out)
318    #[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        /// CMN URI (e.g., cmn://cmn.dev/HASH)
333        uri: String,
334        /// Direction: in (descendants, default) or out (ancestors)
335        #[arg(long, value_enum)]
336        direction: Option<DirectionArg>,
337        /// Synapse server (domain or URL, default: configured default)
338        #[arg(long)]
339        synapse: Option<String>,
340        /// Auth token for synapse (overrides configured token)
341        #[arg(long)]
342        synapse_token_secret: Option<String>,
343        /// Maximum traversal depth (default: 10)
344        #[arg(long, default_value = "10")]
345        max_depth: u32,
346    },
347
348    /// Search for spores by keyword (semantic search via Synapse)
349    #[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        /// Search query text
357        query: String,
358        /// Synapse server (domain or URL, default: configured default)
359        #[arg(long)]
360        synapse: Option<String>,
361        /// Auth token for synapse (overrides configured token)
362        #[arg(long)]
363        synapse_token_secret: Option<String>,
364        /// Filter by domain
365        #[arg(long)]
366        domain: Option<String>,
367        /// Filter by license (SPDX identifier)
368        #[arg(long)]
369        license: Option<String>,
370        /// Filter by bond relationship (format: relation:uri, comma-separated for AND)
371        #[arg(long)]
372        bonds: Option<String>,
373        /// Maximum results (default: 20)
374        #[arg(long, default_value = "20")]
375        limit: u32,
376    },
377
378    // ═══════════════════════════════════════════
379    // Infrastructure Commands
380    // ═══════════════════════════════════════════
381    /// Manage local mycelium site
382    Mycelium {
383        #[command(subcommand)]
384        #[serde(flatten)]
385        action: MyceliumAction,
386    },
387
388    /// Manage Synapse node connections
389    Synapse {
390        #[command(subcommand)]
391        #[serde(flatten)]
392        action: SynapseAction,
393    },
394
395    /// Manage local cache
396    Cache {
397        #[command(subcommand)]
398        #[serde(flatten)]
399        action: CacheAction,
400    },
401
402    /// View or modify hypha configuration
403    Config {
404        #[command(subcommand)]
405        #[serde(flatten)]
406        action: ConfigAction,
407    },
408
409    /// Install, uninstall, or inspect the bundled Hypha agent skill
410    #[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    /// Report whether the bundled Hypha skill is installed and current
427    Status(SkillOptionsArg),
428    /// Install or refresh the bundled Hypha skill
429    Install(SkillOptionsArg),
430    /// Remove the bundled Hypha skill
431    Uninstall(SkillOptionsArg),
432}
433
434#[derive(Args, Serialize)]
435pub struct SkillOptionsArg {
436    /// Agent target to manage
437    #[arg(long, value_enum, default_value = "all")]
438    pub agent: SkillAgentArg,
439    /// Install scope
440    #[arg(long, value_enum, default_value = "personal")]
441    pub scope: SkillScopeArg,
442    /// Explicit skills directory; requires a single --agent
443    #[arg(long)]
444    pub skills_dir: Option<String>,
445    /// Overwrite or remove an unmanaged skill at the target path
446    #[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/// Preferred distribution source for spawn/grow.
467#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
468#[serde(rename_all = "lowercase")]
469pub enum DistArg {
470    Archive,
471    Git,
472}
473
474impl DistArg {
475    /// Canonical string passed to the library layer.
476    pub fn as_str(self) -> &'static str {
477        match self {
478            Self::Archive => "archive",
479            Self::Git => "git",
480        }
481    }
482}
483
484/// Version-control initialization choice for spawn.
485#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
486#[serde(rename_all = "lowercase")]
487pub enum VcsArg {
488    Git,
489    None,
490}
491
492impl VcsArg {
493    /// Canonical string passed to the library layer.
494    pub fn as_str(self) -> &'static str {
495        match self {
496            Self::Git => "git",
497            Self::None => "none",
498        }
499    }
500}
501
502/// Lineage traversal direction.
503#[derive(Clone, Copy, Debug, Serialize, ValueEnum)]
504#[serde(rename_all = "lowercase")]
505pub enum DirectionArg {
506    In,
507    Out,
508}
509
510impl DirectionArg {
511    /// Canonical string passed to the library layer.
512    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    /// Manage bonds in spore.core.json
524    #[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    /// Manage tree configuration in spore.core.json
538    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    /// Add or update a bond (upsert by URI)
549    Set {
550        /// Bond URI (match key)
551        #[arg(long)]
552        uri: String,
553        /// Bond relation (required for new bonds)
554        #[arg(long)]
555        relation: Option<substrate::BondRelation>,
556        /// Bond id
557        #[arg(long)]
558        id: Option<String>,
559        /// Bond reason
560        #[arg(long)]
561        reason: Option<String>,
562        /// Bond parameters (KEY=VALUE, value is parsed as JSON; repeatable)
563        #[arg(long = "with", value_name = "KEY=VALUE")]
564        with_entries: Vec<String>,
565    },
566    /// Remove bonds by URI and/or relation
567    Remove {
568        /// Remove bonds matching this URI
569        #[arg(long)]
570        uri: Option<String>,
571        /// Remove bonds matching this relation
572        #[arg(long)]
573        relation: Option<substrate::BondRelation>,
574    },
575    /// Remove all bonds
576    Clear,
577    /// Reconcile a relation's bonds to exactly match a declarative spec (idempotent)
578    #[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        /// Bond relation to reconcile; every other relation is left untouched
610        #[arg(long)]
611        relation: substrate::BondRelation,
612        /// Path to a JSON spec file, or "-" to read the spec from stdin
613        #[arg(long)]
614        spec: String,
615        /// Domain used to resolve spec entries that omit "uri" (deployed inventory + dry-run fallback)
616        #[arg(long)]
617        domain: Option<String>,
618        /// Custom site directory used for resolution (default: ~/.cmn/mycelium/<domain>)
619        #[arg(long)]
620        site_path: Option<String>,
621        /// Compute the diff without writing; exit 7 (distinct from the error exit) if there is drift
622        #[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 tree configuration fields
631    Set {
632        /// Hash algorithm (e.g., blob_tree_blake3_nfc)
633        #[arg(long)]
634        algorithm: Option<String>,
635        /// File/directory names to exclude from hashing (repeatable)
636        #[arg(long, num_args = 1..)]
637        exclude_names: Option<Vec<String>>,
638        /// Ignore-rule files to follow (repeatable)
639        #[arg(long, num_args = 1..)]
640        follow_rules: Option<Vec<String>>,
641    },
642    /// Show current tree configuration
643    Show,
644}
645
646#[derive(Subcommand, Serialize)]
647#[serde(tag = "action", rename_all = "snake_case")]
648pub enum MyceliumAction {
649    /// Establish a new site for a domain (or update existing)
650    #[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 name (auto-computed when --hub is used)
673        domain: Option<String>,
674        /// Hub domain (e.g., cmnhub.com). Generates a key, computes subdomain
675        /// from pubkey (ed-<base32>), and sets domain + endpoints automatically.
676        #[arg(long, conflicts_with = "endpoints_base")]
677        hub: Option<String>,
678        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
679        #[arg(long)]
680        site_path: Option<String>,
681        /// Site or author name
682        #[arg(long)]
683        name: Option<String>,
684        /// Brief description of the site or author
685        #[arg(long)]
686        synopsis: Option<String>,
687        /// Bio (markdown)
688        #[arg(long)]
689        bio: Option<String>,
690        /// Base URL for endpoints (e.g., https://example.com)
691        #[arg(long)]
692        endpoints_base: Option<String>,
693    },
694    /// Show site status
695    #[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 name (optional, lists all if not specified)
702        domain: Option<String>,
703        /// Custom site directory
704        #[arg(long)]
705        site_path: Option<String>,
706        /// Spore id to resolve from the local mycelium inventory
707        #[arg(long)]
708        id: Option<String>,
709    },
710    /// Start a local HTTP server to serve the site (for debugging)
711    #[command(after_long_help = "\
712Examples:
713  hypha mycelium serve
714  hypha mycelium serve cmn.dev --port 3000")]
715    Serve {
716        /// Domain name
717        domain: Option<String>,
718        /// Custom site directory
719        #[arg(long)]
720        site_path: Option<String>,
721        /// Port to listen on (default: 8080)
722        #[arg(long, default_value = "8080")]
723        port: u16,
724    },
725    /// Manage nutrient methods (add/remove/clear)
726    #[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    /// Manage published spores in the inventory (yank/unyank)
738    #[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    /// Send a pulse to a synapse indexer
752    #[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        /// Synapse server (domain or URL, default: configured default)
758        #[arg(long)]
759        synapse: Option<String>,
760        /// Auth token for synapse (overrides configured token)
761        #[arg(long)]
762        synapse_token_secret: Option<String>,
763        /// Path to signed mycelium.json
764        #[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 or update a nutrient method (upsert by type)
773    Add {
774        /// Domain name
775        domain: String,
776        /// Nutrient method type (e.g., lightning_address, url, evm, solana)
777        #[arg(long = "type", value_name = "TYPE")]
778        method_type: String,
779        /// Nutrient parameters (KEY=VALUE, value is parsed as JSON; repeatable)
780        #[arg(long = "with", value_name = "KEY=VALUE")]
781        with_entries: Vec<String>,
782        /// Custom site directory
783        #[arg(long)]
784        site_path: Option<String>,
785    },
786    /// Remove a nutrient method by type
787    Remove {
788        /// Domain name
789        domain: String,
790        /// Nutrient method type to remove
791        #[arg(long = "type", value_name = "TYPE")]
792        method_type: String,
793        /// Custom site directory
794        #[arg(long)]
795        site_path: Option<String>,
796    },
797    /// Remove all nutrient methods
798    Clear {
799        /// Domain name
800        domain: String,
801        /// Custom site directory
802        #[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    /// Yank a spore from the inventory (delist; keeps published bytes)
811    #[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        /// Spore id to yank from the inventory
826        #[arg(long)]
827        id: String,
828        /// Domain (default: inferred from the site; only needed to disambiguate ~/.cmn sites)
829        #[arg(long)]
830        domain: Option<String>,
831        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
832        #[arg(long)]
833        site_path: Option<String>,
834        /// Also delete the spore's published manifest + archive files
835        #[arg(long)]
836        purge: bool,
837    },
838    /// Unyank a previously yanked spore (re-add from its kept manifest)
839    #[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        /// Spore id to restore to the inventory
848        #[arg(long)]
849        id: String,
850        /// Domain (default: inferred from the site; only needed to disambiguate ~/.cmn sites)
851        #[arg(long)]
852        domain: Option<String>,
853        /// Custom site directory (default: ~/.cmn/mycelium/<domain>)
854        #[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    /// Discover Synapse instances via the network
863    #[command(after_long_help = "\
864Examples:
865  hypha synapse discover
866  hypha synapse discover --synapse https://synapse.cmn.dev")]
867    Discover {
868        /// Synapse to query (domain or URL, default: configured default)
869        #[arg(long)]
870        synapse: Option<String>,
871        /// Auth token for synapse (overrides configured token)
872        #[arg(long)]
873        synapse_token_secret: Option<String>,
874    },
875    /// List configured Synapse nodes
876    #[command(after_long_help = "\
877Examples:
878  hypha synapse list")]
879    List,
880    /// Check health of a Synapse instance
881    #[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 domain or URL (default: configured default)
888        synapse: Option<String>,
889        /// Auth token for synapse (overrides configured token)
890        #[arg(long)]
891        synapse_token_secret: Option<String>,
892    },
893    /// Add a Synapse node
894    #[command(after_long_help = "\
895Examples:
896  hypha synapse add https://synapse.cmn.dev")]
897    Add {
898        /// Synapse URL
899        url: String,
900    },
901    /// Remove a Synapse node
902    #[command(after_long_help = "\
903Examples:
904  hypha synapse remove synapse.cmn.dev")]
905    Remove {
906        /// Synapse domain
907        domain: String,
908    },
909    /// Set default Synapse node
910    #[command(after_long_help = "\
911Examples:
912  hypha synapse use synapse.cmn.dev")]
913    Use {
914        /// Synapse domain
915        domain: String,
916    },
917    /// Configure a Synapse node (token, etc.)
918    #[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        /// Synapse domain
924        domain: String,
925        /// Auth token (use empty string to clear)
926        #[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    /// List all cached spores
935    #[command(after_long_help = "\
936Examples:
937  hypha cache list
938  hypha cache list -o yaml")]
939    List,
940    /// Remove old or all cached items
941    #[command(after_long_help = "\
942Examples:
943  hypha cache clean --all")]
944    Clean {
945        /// Remove all cached items
946        #[arg(long)]
947        all: bool,
948    },
949    /// Show local filesystem path for a cached spore
950    #[command(after_long_help = concat!(
951        "Examples:\n",
952        "  hypha cache path cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
953    ))]
954    Path {
955        /// CMN URI (e.g., cmn://cmn.dev/HASH)
956        uri: String,
957    },
958}
959
960#[derive(Subcommand, Serialize)]
961#[serde(tag = "action", rename_all = "snake_case")]
962pub enum ConfigAction {
963    /// Show current configuration (merged defaults + config.toml)
964    #[command(after_long_help = "\
965Examples:
966  hypha config list
967  hypha config list -o yaml")]
968    List,
969    /// Set a configuration value
970    #[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        /// Config key (dotted path, e.g. cache.cmn_ttl_s)
1010        key: String,
1011        /// Value to set
1012        value: String,
1013    },
1014}
1015
1016/// Render a value as a redacted single-line JSON CLI message.
1017pub(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
1025/// Build hypha's CLI argument-error envelope.
1026///
1027/// Build a protocol-v1 error event for argument/parse failures.
1028pub(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    let build = match env!("GIT_SHA") {
1051        "unknown" => None,
1052        sha => Some(sha),
1053    };
1054    match agent_first_data::cli_handle_version_or_continue(
1055        &raw,
1056        &Cli::command(),
1057        "hypha",
1058        Some(env!("DISPLAY_NAME")),
1059        env!("CARGO_PKG_VERSION"),
1060        build,
1061    ) {
1062        Ok(Some(version)) => {
1063            let mut stdout = std::io::stdout();
1064            let _ = std::io::Write::write_all(&mut stdout, version.as_bytes());
1065            std::process::exit(0);
1066        }
1067        Ok(None) => {}
1068        Err(err) => {
1069            let mut stdout = std::io::stdout();
1070            let message = render_cli_json(err.as_value());
1071            let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1072            let _ = std::io::Write::write_all(&mut stdout, b"\n");
1073            std::process::exit(2);
1074        }
1075    }
1076
1077    match agent_first_data::cli_handle_help_or_continue(
1078        &raw,
1079        &Cli::command(),
1080        &agent_first_data::HelpConfig::human_cli_default(),
1081    ) {
1082        Ok(Some(help)) => {
1083            let mut stdout = std::io::stdout();
1084            let _ = std::io::Write::write_all(&mut stdout, help.as_bytes());
1085            std::process::exit(0);
1086        }
1087        Ok(None) => {}
1088        Err(err) => {
1089            let mut stdout = std::io::stdout();
1090            let message = render_cli_json(&err);
1091            let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1092            let _ = std::io::Write::write_all(&mut stdout, b"\n");
1093            std::process::exit(2);
1094        }
1095    }
1096
1097    Cli::try_parse().unwrap_or_else(|e| {
1098        if matches!(
1099            e.kind(),
1100            clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
1101        ) {
1102            let mut stdout = std::io::stdout();
1103            let _ = std::io::Write::write_all(&mut stdout, e.to_string().as_bytes());
1104            std::process::exit(0);
1105        }
1106
1107        let mut stdout = std::io::stdout();
1108        let message = render_cli_json(&cli_error_value(
1109            &e.to_string(),
1110            "run hypha --help to inspect all commands and flags",
1111        ));
1112        let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
1113        let _ = std::io::Write::write_all(&mut stdout, b"\n");
1114        std::process::exit(2);
1115    })
1116}