use anyhow::{Context, Result, bail}; use clap::{Args, Parser, Subcommand}; use geth_config::GethPaths; use geth_control::{ControlRequest, ControlResponse}; use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport}; 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, }, Peer { #[command(subcommand)] command: PeerCommand, }, 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, Service { #[command(subcommand)] command: ServiceCommand, }, } #[derive(Debug, Subcommand)] pub enum ServiceCommand { Install { #[arg(long, default_value = "auto")] manager: String, #[arg(long)] bin: Option, #[arg(long)] start: bool, }, Uninstall { #[arg(long, default_value = "auto")] manager: String, }, Start { #[arg(long, default_value = "auto")] manager: String, }, Stop { #[arg(long, default_value = "auto")] manager: String, }, Status { #[arg(long, default_value = "auto")] manager: String, }, Print { #[arg(long, default_value = "auto")] manager: String, #[arg(long)] bin: Option, }, } #[derive(Debug, Subcommand)] pub enum NodeCommand { Id, Status, } #[derive(Debug, Subcommand)] pub enum PeerCommand { Export { #[arg(long)] out: Option, }, Import { path: PathBuf, }, List, Ping { node: String, }, AuthCheck { node: String, resource: String, capability: String, }, } #[derive(Debug, Subcommand)] pub enum ResourceCommand { List, Create { kind: String, name: String }, } #[derive(Debug, Subcommand)] pub enum KeychainCommand { Init { #[arg(long)] admin_key: Option, #[arg(long)] signing_key: Option, }, Status, } #[derive(Debug, Subcommand)] pub enum AuthCommand { Explain { subject: String, resource: String, capability: String, }, Grant { subject: String, resource: String, capability: String, #[arg(long)] grant_id: Option, }, Revoke { resource: String, grant_id: String, }, } #[derive(Debug, Subcommand)] pub enum SecretCommand { Status, Create { resource: String, }, Rotate { resource: String, }, Bearer { #[command(subcommand)] command: SecretBearerCommand, }, } #[derive(Debug, Subcommand)] pub enum SecretBearerCommand { Create { resource: String, #[arg(long = "capability", required = true)] capabilities: Vec, #[arg(long)] expires_at_ms: Option, }, List, Challenge { resource: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Prove { token: String, resource: String, #[arg(long)] nonce: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Verify { token: String, resource: String, #[arg(long)] nonce: String, #[arg(long)] response: String, #[arg(long = "capability", required = true)] capabilities: Vec, }, Revoke { resource: String, bearer_id: String, }, } #[derive(Debug, Subcommand)] pub enum CasCommand { Add { path: PathBuf, }, Get { hash: String, #[arg(long)] out: PathBuf, }, Fetch { node: String, hash: String, #[arg(long)] bearer_secret: Option, }, Hash { path: PathBuf, }, Has { hash: String, }, Pin { hash: String, }, Unpin { hash: String, }, Cleanup { #[arg(long)] dry_run: bool, }, Providers { hash: String, }, List, Root { #[command(subcommand)] command: CasRootCommand, }, Conflict { #[command(subcommand)] command: CasConflictCommand, }, } #[derive(Debug, Subcommand)] pub enum CasRootCommand { Add { name: String, path: PathBuf, }, List, Scan { name: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum CasConflictCommand { Record { root: String, path: String, kind: String, #[arg(long)] detail: String, #[arg(long)] base_tree: Option, #[arg(long)] local_tree: Option, #[arg(long)] remote_tree: Option, }, List { #[arg(long)] root: Option, }, Resolve { conflict_id: String, resolution: String, #[arg(long)] note: Option, }, } #[derive(Debug, Subcommand)] pub enum KvCommand { Create { name: String, }, Set { name: String, key: String, value: String, #[arg(long)] subject: Option, }, Get { name: String, key: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum PubsubCommand { Pub { topic: String, message: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, Sub { topic: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum PipeCommand { Listen { name: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, Connect { target: String, #[arg(long)] node: Option, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum DbCommand { Add { name: String, path: PathBuf, }, Status { name: String, }, Changes { name: String, #[arg(long)] after_db_version: Option, #[arg(long, default_value_t = 100)] limit: u32, }, Sync { node: String, name: String, #[arg(long, default_value_t = 100)] limit: u32, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum DocumentCommand { Create { name: String, }, Status { name: String, }, Set { name: String, state_json: String, }, Get { name: String, }, Sync { node: String, name: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum SshCommand { Proxy { node: String, #[arg(long)] bearer_secret: Option, }, Cert { #[command(subcommand)] command: SshCertCommand, }, Revocation { #[command(subcommand)] command: SshRevocationCommand, }, } #[derive(Debug, Subcommand)] pub enum SshCertCommand { Request { #[arg(long)] public_key: PathBuf, #[arg(long, default_value = "user")] kind: String, #[arg(long = "principal", required = true)] principals: Vec, #[arg(long)] valid_for: Option, #[arg(long)] renewal_of: Option, #[arg(long)] reason: Option, #[arg(long)] subject: Option, }, Requests { #[arg(long)] subject: Option, }, Approve { request_id: String, #[arg(long)] ca_key: PathBuf, #[arg(long)] valid_for: Option, #[arg(long)] serial: Option, #[arg(long)] out: Option, #[arg(long)] sign: bool, #[arg(long)] subject: Option, }, Import { request_id: String, #[arg(long)] cert: PathBuf, #[arg(long)] subject: Option, }, List { #[arg(long)] subject: Option, }, Sync { node: String, #[arg(long)] bearer_secret: Option, }, } #[derive(Debug, Subcommand)] pub enum SshRevocationCommand { Add { kind: String, target: String, #[arg(long)] reason: Option, #[arg(long)] subject: Option, }, List { #[arg(long)] subject: Option, }, Export { #[arg(long)] out: PathBuf, #[arg(long, default_value = "jsonl")] format: String, #[arg(long)] ca_public: Option, #[arg(long)] subject: Option, }, Import { path: PathBuf, #[arg(long, default_value = "jsonl")] format: String, #[arg(long)] subject: Option, }, Sync { node: String, #[arg(long)] bearer_secret: Option, }, } #[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::Daemon { command: DaemonCommand::Service { command }, } => { let report = run_service_command(&paths, command).context("manage geth user service")?; print_service_report(report, cli.json || cli.jsonl)?; } 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::Peer { command } => match command { PeerCommand::Export { out } => ControlRequest::PeerCardExport { out }, PeerCommand::Import { path } => ControlRequest::PeerCardImport { path }, PeerCommand::List => ControlRequest::PeerCardList, PeerCommand::Ping { node } => ControlRequest::PeerPing { node }, PeerCommand::AuthCheck { node, resource, capability, } => ControlRequest::PeerAuthCheck { node, resource, capability, }, }, Command::Resource { command: ResourceCommand::List, } => ControlRequest::ResourceList, Command::Resource { command: ResourceCommand::Create { kind, name }, } => ControlRequest::ResourceCreate { kind, name }, Command::Keychain { command: KeychainCommand::Init { admin_key, signing_key, }, } => ControlRequest::KeychainInit { admin_key_path: admin_key, signing_key_path: signing_key, }, Command::Keychain { command: KeychainCommand::Status, } => ControlRequest::KeychainStatus, Command::Auth { command: AuthCommand::Explain { subject, resource, capability, }, } => ControlRequest::AuthExplain { subject, resource, capability, }, Command::Auth { command: AuthCommand::Grant { subject, resource, capability, grant_id, }, } => ControlRequest::AuthGrant { subject, resource, capability, grant_id, }, Command::Auth { command: AuthCommand::Revoke { resource, grant_id }, } => ControlRequest::AuthRevoke { resource, grant_id }, Command::Secret { command } => match command { SecretCommand::Status => ControlRequest::SecretStatus, SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource }, SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource }, SecretCommand::Bearer { command } => match command { SecretBearerCommand::Create { resource, capabilities, expires_at_ms, } => ControlRequest::SecretBearerCreate { resource, capabilities, expires_at_ms, }, SecretBearerCommand::List => ControlRequest::SecretBearerList, SecretBearerCommand::Challenge { resource, capabilities, } => ControlRequest::SecretBearerChallenge { resource, capabilities, }, SecretBearerCommand::Prove { token, resource, capabilities, nonce, } => ControlRequest::SecretBearerProve { secret: token, resource, capabilities, nonce, }, SecretBearerCommand::Verify { token, resource, capabilities, nonce, response, } => ControlRequest::SecretBearerVerify { secret: token, resource, capabilities, nonce, response, }, SecretBearerCommand::Revoke { resource, bearer_id, } => ControlRequest::SecretBearerRevoke { resource, secret: bearer_id, }, }, }, Command::Cas { command } => match command { CasCommand::Add { path } => ControlRequest::CasAdd { path }, CasCommand::Get { hash, out } => ControlRequest::CasGet { hash: hash.into(), out, }, CasCommand::Fetch { node, hash, bearer_secret, } => ControlRequest::CasFetch { node, hash: hash.into(), bearer_secret, }, CasCommand::Hash { path } => ControlRequest::CasHash { path }, CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() }, CasCommand::Pin { hash } => ControlRequest::CasPin { hash: hash.into() }, CasCommand::Unpin { hash } => ControlRequest::CasUnpin { hash: hash.into() }, CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run }, CasCommand::Providers { hash } => ControlRequest::CasProviders { hash: hash.into() }, CasCommand::List => ControlRequest::CasList, CasCommand::Root { command } => match command { CasRootCommand::Add { name, path } => ControlRequest::CasRootAdd { name, path }, CasRootCommand::List => ControlRequest::CasRootList, CasRootCommand::Scan { name } => ControlRequest::CasRootScan { name }, CasRootCommand::Sync { node, name, bearer_secret, } => ControlRequest::CasRootSync { node, name, bearer_secret, }, }, CasCommand::Conflict { command } => match command { CasConflictCommand::Record { root, path, kind, detail, base_tree, local_tree, remote_tree, } => ControlRequest::CasConflictRecord { root, path, kind, detail, base_tree: base_tree.map(Into::into), local_tree: local_tree.map(Into::into), remote_tree: remote_tree.map(Into::into), }, CasConflictCommand::List { root } => ControlRequest::CasConflictList { root }, CasConflictCommand::Resolve { conflict_id, resolution, note, } => ControlRequest::CasConflictResolve { conflict_id, resolution, note, }, }, }, Command::Kv { command } => match command { KvCommand::Create { name } => ControlRequest::KvCreate { name }, KvCommand::Set { name, key, value, subject, } => ControlRequest::KvSet { name, key, value, subject, }, KvCommand::Get { name, key } => ControlRequest::KvGet { name, key }, KvCommand::Sync { node, name, bearer_secret, } => ControlRequest::KvSync { node, name, bearer_secret, }, }, Command::Pubsub { command } => match command { PubsubCommand::Pub { topic, message, node, bearer_secret, } => ControlRequest::PubsubPub { topic, message, node, bearer_secret, }, PubsubCommand::Sub { topic, node, bearer_secret, } => ControlRequest::PubsubSub { topic, node, bearer_secret, }, }, Command::Pipe { command } => match command { PipeCommand::Listen { name, node, bearer_secret, } => ControlRequest::PipeListen { name, node, bearer_secret, }, PipeCommand::Connect { target, node, bearer_secret, } => ControlRequest::PipeConnect { target, node, bearer_secret, }, }, Command::Db { command } => match command { DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path }, DbCommand::Status { name } => ControlRequest::DbStatus { name }, DbCommand::Changes { name, after_db_version, limit, } => ControlRequest::DbChanges { name, after_db_version, limit, }, DbCommand::Sync { node, name, limit, bearer_secret, } => ControlRequest::DbSync { node, name, limit, bearer_secret, }, }, Command::Document { command } => match command { DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name }, DocumentCommand::Status { name } => ControlRequest::DocumentStatus { name }, DocumentCommand::Set { name, state_json } => { ControlRequest::DocumentSet { name, state_json } } DocumentCommand::Get { name } => ControlRequest::DocumentGet { name }, DocumentCommand::Sync { node, name, bearer_secret, } => ControlRequest::DocumentSync { node, name, bearer_secret, }, }, Command::Ssh { command } => match command { SshCommand::Proxy { node, bearer_secret, } => ControlRequest::SshProxyConnect { node, bearer_secret, }, SshCommand::Cert { command } => match command { SshCertCommand::Request { public_key, kind, principals, valid_for, renewal_of, reason, subject, } => ControlRequest::SshCertRequest { public_key_path: public_key, cert_kind: kind, principals, requested_validity: valid_for, renewal_of, reason, subject, }, SshCertCommand::Requests { subject } => ControlRequest::SshCertRequests { subject }, SshCertCommand::Approve { request_id, ca_key, valid_for, serial, out, sign, subject, } => ControlRequest::SshCertApprove { request_id, ca_key_path: ca_key, valid_for, serial, out, sign, subject, }, SshCertCommand::Import { request_id, cert, subject, } => ControlRequest::SshCertImport { request_id, cert_path: cert, subject, }, SshCertCommand::List { subject } => ControlRequest::SshCertList { subject }, SshCertCommand::Sync { node, bearer_secret, } => ControlRequest::SshCertSync { node, bearer_secret, }, }, SshCommand::Revocation { command } => match command { SshRevocationCommand::Add { kind, target, reason, subject, } => ControlRequest::SshRevocationAdd { kind, target, reason, subject, }, SshRevocationCommand::List { subject } => { ControlRequest::SshRevocationList { subject } } SshRevocationCommand::Export { out, format, ca_public, subject, } => ControlRequest::SshRevocationExport { out, format, ca_public, subject, }, SshRevocationCommand::Import { path, format, subject, } => ControlRequest::SshRevocationImport { path, format, subject, }, SshRevocationCommand::Sync { node, bearer_secret, } => ControlRequest::SshRevocationSync { node, bearer_secret, }, }, }, Command::Init | Command::Daemon { .. } => bail!("command is handled directly"), }) } fn run_service_command(paths: &GethPaths, command: ServiceCommand) -> Result { Ok(match command { ServiceCommand::Install { manager, bin, start, } => { geth_node::init_node(paths).context("initialize geth home before service install")?; let manager = manager.parse::()?; let executable = service_executable(bin)?; geth_node::service::install_user_service( paths, ServiceInstallOptions { manager, executable, start, }, )? } ServiceCommand::Uninstall { manager } => { geth_node::service::uninstall_user_service(manager.parse::()?)? } ServiceCommand::Start { manager } => { geth_node::service::start_user_service(manager.parse::()?)? } ServiceCommand::Stop { manager } => { geth_node::service::stop_user_service(manager.parse::()?)? } ServiceCommand::Status { manager } => { geth_node::service::status_user_service(manager.parse::()?)? } ServiceCommand::Print { manager, bin } => { let executable = service_executable(bin)?; geth_node::service::print_user_service( paths, manager.parse::()?, &executable, )? } }) } fn service_executable(bin: Option) -> Result { bin.map(Ok) .unwrap_or_else(std::env::current_exe) .context("resolve current geth executable") } 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!( "endpoint: {}", status.endpoint_id.as_deref().unwrap_or("not started") ); println!("iroh relay: {}", status.iroh_relay_mode); println!( "iroh discovery: {}", if status.iroh_local_discovery { "local-network enabled" } else { "local-network disabled" } ); 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::PeerCardExported { card, out, note } => { println!("peer card: {}", card.node_id); println!("agent: {}", card.agent_id); println!("endpoints: {}", card.endpoints.len()); if let Some(path) = out { println!("wrote: {}", path.display()); } else { println!("{}", serde_json::to_string_pretty(&card)?); } println!("note: {note}"); } ControlResponse::PeerCardImported { peer, note } => { println!("imported peer: {}", peer.card.node_id); println!("agent: {}", peer.card.agent_id); println!("trust: candidate-only"); println!("note: {note}"); } ControlResponse::PeerCardList { peers, note } => { if peers.is_empty() { println!("no peer candidates"); } else { for peer in peers { println!( "{}\t{}\t{} endpoints\tcandidate-only", peer.card.node_id, peer.card.agent_id, peer.card.endpoints.len() ); } } println!("note: {note}"); } ControlResponse::PeerPinged { peer_node_id, peer_agent_id, endpoint_id, alpn, note, } => { println!("peer pong: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("alpn: {alpn}"); println!("note: {note}"); } ControlResponse::PeerAuthChecked { peer_node_id, peer_agent_id, endpoint_id, resource, capability, allowed, reason, evaluated_ops, note, } => { println!("peer auth: {peer_node_id}"); println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("resource: {resource}"); println!("capability: {capability}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("evaluated_ops: {evaluated_ops}"); println!("note: {note}"); } 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::CasFetched { peer_node_id, peer_agent_id, endpoint_id, hash, size_bytes, allowed, reason, note, } => { if allowed { println!("fetched {hash} from {peer_node_id} ({size_bytes} bytes)"); } else { println!("fetch denied for {hash} from {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::CasHash { hash } => println!("{hash}"), ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"), ControlResponse::CasPinned { hash, pinned } => { println!("{hash}: pinned={pinned}"); } ControlResponse::CasCleanup { removed, retained_pinned, dry_run, } => { let action = if dry_run { "would remove" } else { "removed" }; println!("{action}: {}", removed.len()); for hash in removed { println!(" {hash}"); } println!("retained_pinned: {}", retained_pinned.len()); for hash in retained_pinned { println!(" {hash}"); } } ControlResponse::CasList { blobs } => { for blob in blobs { let pin = if blob.pinned { "pinned" } else { "unpinned" }; println!("{}\t{} bytes\t{}", blob.hash, blob.size_bytes, pin); } } ControlResponse::CasProviders { hash, providers } => { println!("hash: {hash}"); println!("providers: {}", providers.len()); for provider in providers { println!( "{}\t{}\t{}", provider.peer_node_id, provider.endpoint_id, provider.last_seen_ms ); } } ControlResponse::CasRootAdded { root } => { println!("added file root: {}", root.name); println!("id: {}", root.id); println!("resource: {}", root.resource); println!("path: {}", root.path); } ControlResponse::CasRootList { roots } => { if roots.is_empty() { println!("no file roots"); } else { for root in roots { println!( "{}\t{}\t{}", root.name, root.path, root.latest_tree .map(|hash| hash.to_string()) .unwrap_or_else(|| "unscanned".to_owned()) ); } } } ControlResponse::CasRootScanned { scan } => { println!("file root: {}", scan.root.name); println!("tree: {}", scan.tree.hash); println!("tree_bytes: {}", scan.tree.size_bytes); println!("changes: {}", scan.changes.len()); for change in scan.changes { println!("{change:?}"); } println!("note: {}", scan.note); } ControlResponse::CasRootSynced { peer_node_id, peer_agent_id, endpoint_id, name, root, tree_bytes_imported, allowed, reason, note, } => { if let Some(root) = root { println!("synced file root: {name}"); println!("peer: {peer_node_id}"); println!("path: {}", root.path); println!( "tree: {}", root.latest_tree .map(|hash| hash.to_string()) .unwrap_or_else(|| "unscanned".to_owned()) ); println!("tree_bytes_imported: {tree_bytes_imported}"); } else { println!("file root sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::CasConflictRecorded { conflict } => { println!("recorded conflict: {}", conflict.id); print_file_conflict(&conflict); } ControlResponse::CasConflictList { conflicts } => { if conflicts.is_empty() { println!("no file conflicts"); } else { for conflict in conflicts { println!( "{}\t{}\t{}\t{}\t{}", conflict.id, conflict.root, conflict.path, conflict.kind.as_str(), conflict.status.as_str() ); } } } ControlResponse::CasConflictResolved { conflict } => { println!("resolved conflict: {}", conflict.id); print_file_conflict(&conflict); } ControlResponse::KeychainStatus(status) => { println!("initialized: {}", status.initialized); println!("admin_keys: {}", status.admin_keys); println!("signatures: {}", status.signatures); println!("verified_signatures: {}", status.verified_signatures); println!("failed_signatures: {}", status.failed_signatures); println!("users: {}", status.users); println!("devices: {}", status.devices); println!("nodes: {}", status.nodes); } ControlResponse::KeychainInitialized { ops, signatures } => { println!("initialized keychain"); for op in ops { println!("recorded keychain op: {}", op.id); } for signature in signatures { println!( "signed keychain op: {} by {} ({})", signature.op_id, signature.signer, signature.namespace ); } } ControlResponse::SecretStatus { secrets } => { if secrets.is_empty() { println!("no resource secrets"); } else { for secret in secrets { println!("{}\t{}\tepoch {}", secret.id, secret.resource, secret.epoch); } } } ControlResponse::SecretCreated { secret } => { println!("resource secret: {}", secret.id); println!("resource: {}", secret.resource); println!("epoch: {}", secret.epoch); } ControlResponse::SecretBearerCreated { access } => { println!("bearer id: {}", access.secret); if let Some(token) = access.token { println!("bearer token: {token}"); } println!("resource: {}", access.resource); println!( "capabilities: {}", access .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); if let Some(expires_at) = access.expires_at { println!("expires_at_ms: {}", expires_at.0); } println!("may_delegate: {}", access.may_delegate); } ControlResponse::SecretBearerList { access } => { if access.is_empty() { println!("no bearer access"); } else { for item in access { println!( "{}\t{}\t{}\tmay_delegate={}", item.secret, item.resource, item.capabilities .iter() .map(ToString::to_string) .collect::>() .join(","), item.may_delegate ); } } } ControlResponse::SecretBearerChallenge { challenge } => { println!("bearer challenge"); println!("resource: {}", challenge.resource); println!("nonce: {}", challenge.nonce); println!("issued_at_ms: {}", challenge.issued_at.0); println!( "capabilities: {}", challenge .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); } ControlResponse::SecretBearerProof { proof } => { println!("bearer proof"); println!("secret: {}", proof.secret); println!("resource: {}", proof.resource); println!("nonce: {}", proof.nonce); println!("response: {}", proof.response); println!( "capabilities: {}", proof .capabilities .iter() .map(ToString::to_string) .collect::>() .join(",") ); } ControlResponse::SecretBearerVerified { secret, resource, capabilities, verified, reason, } => { println!("bearer verified: {verified}"); println!("secret: {secret}"); println!("resource: {resource}"); println!("capabilities: {}", capabilities.join(",")); println!("reason: {reason}"); } ControlResponse::SecretBearerRevoked { resource, secret } => { println!("revoked bearer secret: {secret}"); println!("resource: {resource}"); } 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::AuthOpRecorded { op } => { println!("recorded auth op: {}", op.id); println!("resource: {}", op.resource); } ControlResponse::SshCertRequested { request } => { println!("ssh cert request: {}", request.id); println!("status: {}", request.status); println!("kind: {}", request.cert_kind); println!("principals: {}", request.principals.join(",")); println!("public_key_fingerprint: {}", request.public_key_fingerprint); } ControlResponse::SshCertRequests { requests } => { if requests.is_empty() { println!("no ssh certificate requests"); } else { for request in requests { println!( "{}\t{}\t{}\t{}\t{}", request.id, request.status, request.cert_kind, request.principals.join(","), request.public_key_fingerprint ); } } } ControlResponse::SshCertApproved { approval } => { println!("approved ssh cert request: {}", approval.request_id); println!("valid_for: {}", approval.valid_for); if let Some(serial) = approval.serial { println!("serial: {serial}"); } if let Some(output_path) = approval.output_path { println!("expected_certificate: {output_path}"); } println!("signing_command:"); println!("{}", shell_quote_command(&approval.signing_command)); println!("signed: {}", approval.signed); if let Some(certificate_id) = approval.certificate_id { println!("certificate_id: {certificate_id}"); } println!("note: {}", approval.note); } ControlResponse::SshCertImported { certificate } => { println!("imported ssh certificate: {}", certificate.id); println!("request: {}", certificate.request_id); println!("fingerprint: {}", certificate.certificate_fingerprint); } ControlResponse::SshCertList { requests, certificates, } => { println!("requests:"); if requests.is_empty() { println!(" none"); } else { for request in requests { println!( " {}\t{}\t{}\t{}", request.id, request.status, request.cert_kind, request.principals.join(",") ); } } println!("certificates:"); if certificates.is_empty() { println!(" none"); } else { for certificate in certificates { println!( " {}\t{}\t{}", certificate.id, certificate.request_id, certificate.certificate_fingerprint ); } } } ControlResponse::SshCertSynced { peer_node_id, peer_agent_id, endpoint_id, requests_imported, certificates_imported, allowed, reason, note, } => { if allowed { println!( "synced ssh cert metadata from {peer_node_id}: {requests_imported} requests, {certificates_imported} certificates" ); } else { println!("ssh cert metadata sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::SshRevocationAdded { revocation } => { println!("added ssh revocation: {}", revocation.id); println!("kind: {}", revocation.kind); println!("target: {}", revocation.target); if let Some(reason) = revocation.reason { println!("reason: {reason}"); } } ControlResponse::SshRevocationList { revocations } => { if revocations.is_empty() { println!("no ssh revocations"); } else { for revocation in revocations { println!( "{}\t{}\t{}\t{}", revocation.id, revocation.kind, revocation.target, revocation.reason.unwrap_or_default() ); } } } ControlResponse::SshRevocationExported { out, format, count, note, } => { println!("exported {count} ssh revocations to {}", out.display()); println!("format: {format}"); println!("note: {note}"); } ControlResponse::SshRevocationImported { revocations, format, count, note, } => { println!("imported {count} ssh revocations"); println!("format: {format}"); for revocation in revocations { println!( "{}\t{}\t{}", revocation.id, revocation.kind, revocation.target ); } println!("note: {note}"); } ControlResponse::SshRevocationSynced { peer_node_id, peer_agent_id, endpoint_id, revocations_imported, allowed, reason, note, } => { if allowed { println!("synced {revocations_imported} ssh revocations from {peer_node_id}"); } else { println!("ssh revocation sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::DbAdded { db } => { println!("registered db: {}", db.name); println!("id: {}", db.id); println!("resource: {}", db.resource); println!("path: {}", db.path); println!("sync_status: {}", db.sync_status); } ControlResponse::DbStatus { db } => { println!("db: {}", db.name); println!("id: {}", db.id); println!("resource: {}", db.resource); println!("path: {}", db.path); println!("path_exists: {}", db.path_exists); println!( "size_bytes: {}", db.size_bytes .map(|size| size.to_string()) .unwrap_or_else(|| "unknown".to_owned()) ); println!("schema_metadata: {}", db.schema_metadata); println!( "crsqlite_changes_available: {}", db.crsqlite_changes.available ); if let Some(count) = db.crsqlite_changes.change_count { println!("crsqlite_change_count: {count}"); } if let Some(version) = db.crsqlite_changes.max_db_version { println!("crsqlite_max_db_version: {version}"); } if let Some(error) = db.crsqlite_changes.error { println!("crsqlite_changes_error: {error}"); } println!("sync_status: {}", db.sync_status); } ControlResponse::DbChanges { db, batch } => { println!("db: {}", db.name); println!("changes: {}", batch.changes.len()); println!( "max_db_version: {}", batch .max_db_version .map(|version| version.to_string()) .unwrap_or_else(|| "none".to_owned()) ); println!("schema_metadata: {}", batch.schema_metadata); for change in batch.changes { println!( "{}\t{}\t{}", change.db_version, change.table_name, change.column_id ); } } ControlResponse::DbSynced { peer_node_id, peer_agent_id, endpoint_id, name, changes_received, changes_applied, max_db_version, schema_match, allowed, reason, note, } => { if allowed { println!("synced db changes for {name} from {peer_node_id}"); } else { println!("db sync denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("changes_received: {changes_received}"); println!("changes_applied: {changes_applied}"); println!( "max_db_version: {}", max_db_version .map(|version| version.to_string()) .unwrap_or_else(|| "none".to_owned()) ); println!("schema_match: {schema_match}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::KvCreated { kv } => { println!("created kv: {}", kv.name); println!("id: {}", kv.id); println!("resource: {}", kv.resource); println!("sync_status: {}", kv.sync_status); } ControlResponse::KvSet { entry } => { println!("set {} {}", entry.store, entry.key); } ControlResponse::KvGet { entry } => { if let Some(entry) = entry { println!("{}", entry.value); } else { println!("not found"); } } ControlResponse::KvSynced { peer_node_id, peer_agent_id, endpoint_id, name, entries_imported, allowed, reason, note, } => { if allowed { println!("synced kv {name} from {peer_node_id}: {entries_imported} entries"); } else { println!("kv sync denied for {name} by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::DocumentCreated { document } => { println!("created document: {}", document.name); println!("id: {}", document.id); println!("resource: {}", document.resource); println!("sync_status: {}", document.sync_status); println!("state_bytes: {}", document.state_bytes); } ControlResponse::DocumentStatus { document } => { println!("document: {}", document.name); println!("id: {}", document.id); println!("resource: {}", document.resource); println!("sync_status: {}", document.sync_status); println!("state_bytes: {}", document.state_bytes); } ControlResponse::DocumentSet { state } => { println!("updated document: {}", state.document.name); println!("state_bytes: {}", state.document.state_bytes); println!("updated_at_ms: {}", state.updated_at.0); } ControlResponse::DocumentGet { state } => { println!("{}", state.state_json); } ControlResponse::DocumentSynced { peer_node_id, peer_agent_id, endpoint_id, name, updated, allowed, reason, note, } => { if allowed { println!("synced document {name} from {peer_node_id}: updated={updated}"); } else { println!("document sync denied for {name} by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PubsubPublished { message } => { println!("published: {}", message.topic); println!("published_at_ms: {}", message.published_at.0); } ControlResponse::PubsubRemotePublished { peer_node_id, peer_agent_id, endpoint_id, message, allowed, reason, note, } => { if allowed { println!("published: {}", message.topic); println!("peer: {peer_node_id}"); println!("published_at_ms: {}", message.published_at.0); } else { println!("pubsub publish denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PubsubMessages { topic, messages, note, } => { println!("topic: {topic}"); println!("messages: {}", messages.len()); for message in messages { println!("{}\t{}", message.published_at.0, message.message); } println!("note: {note}"); } ControlResponse::PubsubRemoteMessages { peer_node_id, peer_agent_id, endpoint_id, topic, messages, allowed, reason, note, } => { if allowed { println!("topic: {topic}"); println!("peer: {peer_node_id}"); println!("messages: {}", messages.len()); for message in messages { println!("{}\t{}", message.published_at.0, message.message); } } else { println!("pubsub subscribe denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeListening { listener } => { println!("listening pipe: {}", listener.name); println!("id: {}", listener.id); println!("listened_at_ms: {}", listener.listened_at.0); println!("note: {}", listener.note); } ControlResponse::PipeConnected { connection } => { println!("pipe target: {}", connection.target); println!("local_listener_found: {}", connection.local_listener_found); println!("connected_at_ms: {}", connection.connected_at.0); println!("note: {}", connection.note); } ControlResponse::PipeRemoteListening { peer_node_id, peer_agent_id, endpoint_id, listener, allowed, reason, note, } => { if let Some(listener) = listener { println!("listening pipe: {}", listener.name); println!("peer: {peer_node_id}"); println!("id: {}", listener.id); println!("listened_at_ms: {}", listener.listened_at.0); } else { println!("pipe listen denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::PipeRemoteConnected { peer_node_id, peer_agent_id, endpoint_id, connection, allowed, reason, note, } => { if allowed { println!("pipe target: {}", connection.target); println!("peer: {peer_node_id}"); println!("remote_listener_found: {}", connection.local_listener_found); println!("connected_at_ms: {}", connection.connected_at.0); } else { println!("pipe connect denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::SshProxyConnected { peer_node_id, peer_agent_id, endpoint_id, connection, allowed, reason, note, } => { if allowed { println!("ssh proxy target: {peer_node_id}"); if let Some(connection) = connection { println!("connected_at_ms: {}", connection.connected_at.0); if let Some(local_sshd_target) = connection.local_sshd_target { println!("remote_sshd_target: {local_sshd_target}"); } println!( "admin_shell_available: {}", connection.admin_shell_available ); println!("connection_note: {}", connection.note); } } else { println!("ssh proxy denied by {peer_node_id}"); } println!("agent: {peer_agent_id}"); println!("endpoint: {endpoint_id}"); println!("allowed: {allowed}"); println!("reason: {reason}"); println!("note: {note}"); } ControlResponse::NotImplemented { module, command } => { println!("{module} {command}: not implemented yet"); } ControlResponse::Error { message } => bail!(message), } Ok(()) } fn print_service_report(report: ServiceReport, json: bool) -> Result<()> { if json { println!( "{}", serde_json::json!({ "manager": report.manager.to_string(), "action": format!("{:?}", report.action), "service_name": report.service_name, "definition_path": report.definition_path, "definition": report.definition, "commands": report.commands, "note": report.note, }) ); return Ok(()); } println!("service: {}", report.service_name); println!("manager: {}", report.manager); println!("action: {:?}", report.action); if let Some(path) = report.definition_path { println!("definition: {}", path.display()); } if !report.commands.is_empty() { println!("commands:"); for command in report.commands { println!(" {}", shell_quote_command(&command)); } } if let Some(definition) = report.definition { println!("definition_body:"); print!("{definition}"); } println!("note: {}", report.note); Ok(()) } fn print_file_conflict(conflict: &geth_cas::FileConflict) { println!("root: {}", conflict.root); println!("resource: {}", conflict.resource); println!("path: {}", conflict.path); println!("kind: {}", conflict.kind.as_str()); println!("status: {}", conflict.status.as_str()); if let Some(hash) = &conflict.base_tree { println!("base_tree: {hash}"); } if let Some(hash) = &conflict.local_tree { println!("local_tree: {hash}"); } if let Some(hash) = &conflict.remote_tree { println!("remote_tree: {hash}"); } println!("detail: {}", conflict.detail); if let Some(resolution) = &conflict.resolution { println!("resolution: {}", resolution.as_str()); } if let Some(note) = &conflict.resolution_note { println!("resolution_note: {note}"); } } fn shell_quote_command(command: &[String]) -> String { command .iter() .map(|arg| { if arg .bytes() .all(|byte| byte.is_ascii_alphanumeric() || b"-_./:=+@,".contains(&byte)) { arg.clone() } else { format!("'{}'", arg.replace('\'', "'\\''")) } }) .collect::>() .join(" ") }