use std::path::PathBuf; use anyhow::{Result, bail}; use clap::{Parser, Subcommand, ValueEnum}; use frxd::config::Config; use frxd::crypto::Keypair; use frxd::message::{EXPOSURE_FULL, EXPOSURE_METADATA}; use frxd::{commands, node, onboard, relay}; #[derive(Parser)] #[command( name = "frxd", version, about = "FRX member node — querier, responder, and local index (Draft 0.5)" )] struct Cli { #[arg(long, global = true, default_value = "frxd.toml")] config: PathBuf, #[arg(long)] onboarding: bool, #[arg(long)] listen: Option, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Command { Init { #[arg(long, default_value = "member")] name: String, #[arg(long, default_value = "127.0.0.1:7701")] listen: String, #[arg(long, default_value = "http://127.0.0.1:7700")] relay: Vec, #[arg(long, default_value = "./frx-data")] data_dir: String, #[arg(long)] force: bool, #[arg(long)] id: Option, #[arg(long)] registry: Option, #[arg(long)] ma_key: Option, #[arg(long)] ca_cert: Option, #[arg(long)] allow_insecure: bool, }, Add { path: PathBuf, #[arg(long)] name: Option, #[arg(long)] shared: bool, #[arg(long, value_enum, default_value_t = Exposure::Full)] exposure: Exposure, }, Reindex, Search { text: String, #[arg(long, default_value_t = 10)] limit: usize, }, Query { text: String, #[arg(long)] max_results: Option, #[arg(long)] timeout_ms: Option, #[arg(long)] local_only: bool, }, Serve, Relay { #[arg(long, default_value = "127.0.0.1:7700")] listen: String, #[arg(long, default_value_t = relay::DEFAULT_CAPACITY)] capacity: usize, #[arg(long = "peer")] peers: Vec, #[arg(long)] url: Option, #[arg(long)] registry: Option, #[arg(long)] ma_key: Option, #[arg(long)] ca_cert: Option, #[arg(long)] allow_insecure: bool, #[arg(long, default_value_t = 3)] max_hops: usize, }, Status, Member { #[command(subcommand)] command: MemberCommand, }, Key { #[command(subcommand)] command: KeyCommand, }, Registry { #[arg(long, default_value = "./frx-registry")] dir: PathBuf, #[command(subcommand)] command: RegistryCommand, }, Aggregates { #[arg(long)] from: Option, #[arg(long)] period: Option, #[arg(long)] timeout_ms: Option, }, } #[derive(Subcommand)] enum MemberCommand { Add { name: String, pubkey: String, #[arg(long, default_value = "source")] class: String, #[arg(long = "previous")] previous: Vec, }, Remove { name: String, }, List, } #[derive(Subcommand)] enum KeyCommand { Show, ShowEnc, Rotate, } #[derive(Subcommand)] enum RegistryCommand { Init { #[arg(long, default_value = "frx.invalid")] zone: String, }, Add { id: String, pubkey: String, #[arg(long, default_value = "source")] class: String, #[arg(long)] not_before: Option, #[arg(long)] not_after: Option, #[arg(long)] enc_key: Option, }, SetEncKey { id: String, enc_key: String, }, AddKey { id: String, pubkey: String, #[arg(long)] not_before: Option, #[arg(long)] not_after: Option, }, RevokeKey { id: String, pubkey: String, }, Remove { id: String, }, List, Applications, Approve { id: String, #[arg(long)] registry_url: Option, }, Invite { id: String, #[arg(long)] registry_url: Option, }, SetRelays { #[arg(required = true)] relays: Vec, }, Show, Serve { #[arg(long, default_value = "127.0.0.1:7800")] listen: String, }, } #[derive(Clone, Copy, ValueEnum)] enum Exposure { Metadata, Full, } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); if cli.onboarding { let mut input = std::io::stdin().lock(); let mut output = std::io::stdout().lock(); onboard::run(&mut input, &mut output, &cli.config, cli.listen.as_deref()).await?; return Ok(()); } let Some(command) = cli.command else { bail!("no subcommand given (try --onboarding or --help)"); }; match command { Command::Init { name, listen, relay, data_dir, force, id, registry, ma_key, ca_cert, allow_insecure, } => { if cli.config.exists() && !force { bail!( "config {} already exists (use --force to overwrite)", cli.config.display() ); } if registry.is_some() && ma_key.is_none() { bail!("--ma-key is required with --registry"); } let mut config = Config::new(&name, &listen, relay, &data_dir); config.node.id = id; config.node.registry = registry; config.node.ma_key = ma_key; config.node.ca_cert = ca_cert; config.node.allow_insecure = allow_insecure; if config.node.registry.is_none() { config.node.dev_bootstrap = true; println!( "warning: no registry configured; development open bootstrap (any valid key is accepted; do not deploy)" ); } let key = Keypair::generate(); config.save_key(&key)?; let (enc_secret, enc_public) = frxd::crypto::generate_enc_keypair(); config.save_enc_key(&enc_secret)?; config.save(&cli.config)?; println!("wrote config {}", cli.config.display()); println!("wrote key {}", config.key_path().display()); println!("pubkey {}", key.public_hex()); println!("enc pubkey {enc_public}"); println!("data dir {}", config.data_dir().display()); } Command::Add { path, name, shared, exposure, } => { let exposure = match exposure { Exposure::Metadata => EXPOSURE_METADATA, Exposure::Full => EXPOSURE_FULL, }; commands::add(&cli.config, &path, name, shared, exposure)?; } Command::Reindex => commands::reindex(&cli.config)?, Command::Search { text, limit } => commands::search(&cli.config, &text, limit).await?, Command::Query { text, max_results, timeout_ms, local_only, } => { commands::query(&cli.config, &text, max_results, timeout_ms, !local_only).await?; } Command::Serve => { let config = Config::load(&cli.config)?; let handle = node::Node::start(config).await?; println!("frxd listening on http://{}", handle.addr); println!("pubkey {}", handle.pubkey); println!("control API POST http://{}/v1/local/query", handle.addr); tokio::signal::ctrl_c().await?; } Command::Relay { listen, capacity, peers, url, registry, ma_key, ca_cert, allow_insecure, max_hops, } => { let options = relay::RelayOptions { capacity, peers, url, registry, ma_key, ca_cert, allow_insecure, max_hops, }; let (listener, addr) = relay::bind(&listen).await?; println!("relay listening on http://{addr} (capacity {capacity})"); relay::run(listener, options).await?; } Command::Status => commands::status(&cli.config).await?, Command::Member { command } => match command { MemberCommand::Add { name, pubkey, class, previous, } => commands::member_add(&cli.config, &name, &pubkey, &class, &previous)?, MemberCommand::Remove { name } => commands::member_remove(&cli.config, &name)?, MemberCommand::List => commands::member_list(&cli.config)?, }, Command::Key { command } => match command { KeyCommand::Show => commands::key_show(&cli.config)?, KeyCommand::ShowEnc => commands::key_show_enc(&cli.config)?, KeyCommand::Rotate => commands::key_rotate(&cli.config)?, }, Command::Registry { dir, command } => match command { RegistryCommand::Init { zone } => commands::registry_init(&dir, &zone)?, RegistryCommand::Add { id, pubkey, class, not_before, not_after, enc_key, } => { commands::registry_add(&dir, &id, &pubkey, &class, not_before, not_after, enc_key)? } RegistryCommand::SetEncKey { id, enc_key } => { commands::registry_set_enc_key(&dir, &id, &enc_key)? } RegistryCommand::AddKey { id, pubkey, not_before, not_after, } => commands::registry_add_key(&dir, &id, &pubkey, not_before, not_after)?, RegistryCommand::RevokeKey { id, pubkey } => { commands::registry_revoke_key(&dir, &id, &pubkey)? } RegistryCommand::Remove { id } => commands::registry_remove(&dir, &id)?, RegistryCommand::List => commands::registry_list(&dir)?, RegistryCommand::Applications => commands::registry_applications(&dir)?, RegistryCommand::Approve { id, registry_url } => { commands::registry_approve(&dir, &id, registry_url.as_deref())? } RegistryCommand::Invite { id, registry_url } => { commands::registry_invite(&dir, &id, registry_url.as_deref())? } RegistryCommand::SetRelays { relays } => commands::registry_set_relays(&dir, &relays)?, RegistryCommand::Show => commands::registry_show(&dir)?, RegistryCommand::Serve { listen } => commands::registry_serve(&dir, &listen).await?, }, Command::Aggregates { from, period, timeout_ms, } => { commands::aggregates(&cli.config, from.as_deref(), period.as_deref(), timeout_ms) .await? } } Ok(()) }