use anyhow::{Context, Result, bail}; use clap::{Args, Parser, Subcommand}; use geth_config::GethPaths; use geth_control::{ControlRequest, ControlResponse}; use std::path::PathBuf; #[derive(Debug, Parser)] #[command(name = "geth", about = "Personal local-first Iroh mesh runtime")] pub struct Cli { #[arg(long, global = true)] pub json: bool, #[arg(long, global = true)] pub jsonl: bool, #[command(subcommand)] pub command: Command, } #[derive(Debug, Subcommand)] pub enum Command { Init, Daemon { #[command(subcommand)] command: DaemonCommand, }, Status, Node { #[command(subcommand)] command: NodeCommand, }, Resource { #[command(subcommand)] command: ResourceCommand, }, Keychain { #[command(subcommand)] command: KeychainCommand, }, Auth { #[command(subcommand)] command: AuthCommand, }, Secret { #[command(subcommand)] command: SecretCommand, }, Cas { #[command(subcommand)] command: CasCommand, }, Kv { #[command(subcommand)] command: KvCommand, }, Pubsub { #[command(subcommand)] command: PubsubCommand, }, Pipe { #[command(subcommand)] command: PipeCommand, }, Db { #[command(subcommand)] command: DbCommand, }, Document { #[command(subcommand)] command: DocumentCommand, }, Ssh { #[command(subcommand)] command: SshCommand, }, } #[derive(Debug, Subcommand)] pub enum DaemonCommand { Run, } #[derive(Debug, Subcommand)] pub enum NodeCommand { Id, Status, } #[derive(Debug, Subcommand)] pub enum ResourceCommand { List, Create { kind: String, name: String }, } #[derive(Debug, Subcommand)] pub enum KeychainCommand { Init, Status, } #[derive(Debug, Subcommand)] pub enum AuthCommand { Explain { subject: String, resource: String, capability: String, }, } #[derive(Debug, Subcommand)] pub enum SecretCommand { Status, } #[derive(Debug, Subcommand)] pub enum CasCommand { Add { path: PathBuf, }, Get { hash: String, #[arg(long)] out: PathBuf, }, Hash { path: PathBuf, }, Has { hash: String, }, List, } #[derive(Debug, Subcommand)] pub enum KvCommand { Create { name: String, }, Set { name: String, key: String, value: String, }, Get { name: String, key: String, }, } #[derive(Debug, Subcommand)] pub enum PubsubCommand { Pub { topic: String, message: String }, Sub { topic: String }, } #[derive(Debug, Subcommand)] pub enum PipeCommand { Listen { name: String }, Connect { target: String }, } #[derive(Debug, Subcommand)] pub enum DbCommand { Add { name: String, path: PathBuf }, Status { name: String }, } #[derive(Debug, Subcommand)] pub enum DocumentCommand { Create { name: String }, Status { name: String }, } #[derive(Debug, Subcommand)] pub enum SshCommand { Proxy { node: String }, } #[derive(Debug, Args)] pub struct EmptyArgs {} pub async fn run() -> Result<()> { let cli = Cli::parse(); let paths = GethPaths::resolve().context("resolve geth paths")?; match cli.command { Command::Init => { let node = geth_node::init_node(&paths).context("initialize geth node")?; println!("initialized geth home: {}", node.paths.home().display()); println!("agent: {}", node.agent_id); println!("node: {}", node.node_id); } Command::Daemon { command: DaemonCommand::Run, } => { geth_node::run_daemon(paths) .await .context("run geth daemon")?; } command => { let request = request_for_command(command)?; let response = geth_node::send_control(&paths, request) .await .with_context(|| { format!("connect to daemon at {}", paths.socket_path().display()) })?; print_response(response, cli.json || cli.jsonl)?; } } Ok(()) } fn request_for_command(command: Command) -> Result { Ok(match command { Command::Status => ControlRequest::Status, Command::Node { command: NodeCommand::Id, } => ControlRequest::NodeId, Command::Node { command: NodeCommand::Status, } => ControlRequest::Status, Command::Resource { command: ResourceCommand::List, } => ControlRequest::ResourceList, Command::Resource { command: ResourceCommand::Create { kind, name }, } => ControlRequest::ResourceCreate { kind, name }, Command::Keychain { command: KeychainCommand::Init, } => ControlRequest::ModuleStub { module: "keychain".to_owned(), command: "init".to_owned(), }, Command::Keychain { command: KeychainCommand::Status, } => ControlRequest::KeychainStatus, Command::Auth { command: AuthCommand::Explain { subject, resource, capability, }, } => ControlRequest::AuthExplain { subject, resource, capability, }, Command::Secret { command } => ControlRequest::ModuleStub { module: "secret".to_owned(), command: format!("{command:?}"), }, Command::Cas { command } => match command { CasCommand::Add { path } => ControlRequest::CasAdd { path }, CasCommand::Get { hash, out } => ControlRequest::CasGet { hash: hash.into(), out, }, CasCommand::Hash { path } => ControlRequest::CasHash { path }, CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() }, CasCommand::List => ControlRequest::CasList, }, Command::Kv { command } => ControlRequest::ModuleStub { module: "kv".to_owned(), command: format!("{command:?}"), }, Command::Pubsub { command } => ControlRequest::ModuleStub { module: "pubsub".to_owned(), command: format!("{command:?}"), }, Command::Pipe { command } => ControlRequest::ModuleStub { module: "pipe".to_owned(), command: format!("{command:?}"), }, Command::Db { command } => ControlRequest::ModuleStub { module: "db".to_owned(), command: format!("{command:?}"), }, Command::Document { command } => ControlRequest::ModuleStub { module: "document".to_owned(), command: format!("{command:?}"), }, Command::Ssh { command } => ControlRequest::ModuleStub { module: "ssh-proxy".to_owned(), command: format!("{command:?}"), }, Command::Init | Command::Daemon { .. } => bail!("command is handled directly"), }) } fn print_response(response: ControlResponse, json: bool) -> Result<()> { if json { println!("{}", serde_json::to_string_pretty(&response)?); return Ok(()); } match response { ControlResponse::Status(status) => { println!("geth daemon: running"); println!("home: {}", status.home.display()); println!("socket: {}", status.socket.display()); println!("agent: {}", status.agent_id); println!("node: {}", status.node_id); println!("iroh: {}", status.iroh); } ControlResponse::NodeId(node) => { println!("agent: {}", node.agent_id); println!("node: {}", node.node_id); println!( "endpoint: {}", node.endpoint_id .as_deref() .unwrap_or("not started in bootstrap") ); } ControlResponse::ResourceList { resources } => { if resources.is_empty() { println!("no resources"); } else { for resource in resources { println!("{}\t{}\t{}", resource.kind, resource.name, resource.id); } } } ControlResponse::ResourceCreated { resource } => { println!( "created resource: {} {} ({})", resource.kind, resource.name, resource.id ); } ControlResponse::CasAdded { hash, size_bytes } => { println!("{hash} {size_bytes} bytes"); } ControlResponse::CasGot { hash, out, size_bytes, } => { println!("wrote {hash} to {} ({size_bytes} bytes)", out.display()); } ControlResponse::CasHash { hash } => println!("{hash}"), ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"), ControlResponse::CasList { blobs } => { for blob in blobs { println!("{}\t{} bytes", blob.hash, blob.size_bytes); } } ControlResponse::KeychainStatus(status) => { println!("initialized: {}", status.initialized); println!("admin_keys: {}", status.admin_keys); println!("users: {}", status.users); println!("devices: {}", status.devices); println!("nodes: {}", status.nodes); } ControlResponse::AuthExplain(explain) => { println!("allowed: {}", explain.allowed); println!("subject: {}", explain.subject); println!("resource: {}", explain.resource); println!("capability: {}", explain.capability); println!("reason: {}", explain.reason); println!("evaluated_ops: {}", explain.evaluated_ops); } ControlResponse::NotImplemented { module, command } => { println!("{module} {command}: not implemented yet"); } ControlResponse::Error { message } => bail!(message), } Ok(()) }