Files
lattice/lattice-cli/src/node_commands.rs
T

314 lines
12 KiB
Rust

//! Node commands - operations on the node (mesh, peers, status)
use crate::commands::{block_async, Command, CommandResult, Handler};
use lattice_core::{Node, StoreHandle, PeerStatus, Uuid};
use lattice_net::LatticeServer;
use chrono::DateTime;
use std::time::Instant;
pub fn node_commands() -> Vec<Command> {
vec![
// Store management
Command { name: "init", args: "", desc: "Initialize root store", group: "node", min_args: 0, max_args: 0, handler: cmd_init as Handler },
Command { name: "create-store", args: "", desc: "Create a new store", group: "node", min_args: 0, max_args: 0, handler: cmd_create_store as Handler },
Command { name: "use", args: "<uuid>", desc: "Switch to a store", group: "node", min_args: 1, max_args: 1, handler: cmd_use_store as Handler },
Command { name: "list-stores", args: "", desc: "List all stores", group: "node", min_args: 0, max_args: 0, handler: cmd_list_stores as Handler },
Command { name: "node-status", args: "", desc: "Show node info", group: "node", min_args: 0, max_args: 0, handler: cmd_node_status as Handler },
// Peer management
Command { name: "invite", args: "<pubkey>", desc: "Invite a peer", group: "peers", min_args: 1, max_args: 1, handler: cmd_invite as Handler },
Command { name: "peers", args: "", desc: "List all peers", group: "peers", min_args: 0, max_args: 0, handler: cmd_peers as Handler },
Command { name: "remove", args: "<pubkey>", desc: "Remove a peer", group: "peers", min_args: 1, max_args: 1, handler: cmd_remove as Handler },
// Networking
Command { name: "join", args: "<node_id>", desc: "Join an existing mesh", group: "network", min_args: 1, max_args: 1, handler: cmd_join as Handler },
Command { name: "sync", args: "[node_id]", desc: "Sync with peers", group: "network", min_args: 0, max_args: 1, handler: cmd_sync as Handler },
]
}
// --- Store management ---
fn cmd_init(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, _args: &[String]) -> CommandResult {
match block_async(node.init()) {
Ok(store_id) => {
println!("Initialized with root store: {}", store_id);
println!("Node info stored in /nodes/{}/*", hex::encode(node.node_id()));
match block_async(node.root_store()).as_ref() {
Some(h) => CommandResult::SwitchTo(h.clone()),
None => CommandResult::Ok,
}
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
}
fn cmd_create_store(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, _args: &[String]) -> CommandResult {
match node.create_store() {
Ok(store_id) => {
println!("Created store: {}", store_id);
match block_async(node.open_store(store_id)) {
Ok((handle, _)) => {
println!("Switched to new store");
CommandResult::SwitchTo(handle)
}
Err(e) => {
eprintln!("Warning: {}", e);
CommandResult::Ok
}
}
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
}
fn cmd_use_store(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, args: &[String]) -> CommandResult {
let store_id = match Uuid::parse_str(&args[0]) {
Ok(id) => id,
Err(_) => {
eprintln!("Error: invalid UUID '{}'", args[0]);
return CommandResult::Ok;
}
};
let start = Instant::now();
match block_async(node.open_store(store_id)) {
Ok((handle, info)) => {
if info.entries_replayed > 0 {
println!("Replayed {} entries ({:.2?})", info.entries_replayed, start.elapsed());
} else {
println!("Switched to store {}", store_id);
}
CommandResult::SwitchTo(handle)
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
}
fn cmd_list_stores(node: &Node, store: Option<&StoreHandle>, _server: Option<&LatticeServer>, _args: &[String]) -> CommandResult {
let stores = match node.list_stores() {
Ok(s) => s,
Err(e) => {
eprintln!("Error: {}", e);
return CommandResult::Ok;
}
};
let current_id = store.map(|s| s.id());
if stores.is_empty() {
println!("No stores. Use 'init' or 'create-store'.");
} else {
for store_id in stores {
let marker = if Some(store_id) == current_id { " *" } else { "" };
println!("{}{}", store_id, marker);
}
}
CommandResult::Ok
}
// --- Info ---
fn cmd_node_status(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, _args: &[String]) -> CommandResult {
println!("Node ID: {}", hex::encode(node.node_id()));
if let Some(name) = node.name() {
println!("Name: {}", name);
}
println!("Data: {}", node.data_path().display());
match node.root_store_id() {
Ok(Some(id)) => println!("Root: {}", id),
Ok(None) => println!("Root: (not set)"),
Err(_) => println!("Root: (error)"),
}
// Count peers using node.list_peers()
if let Ok(peers) = block_async(node.list_peers()) {
let active = peers.iter().filter(|p| p.status == PeerStatus::Active).count();
let invited = peers.iter().filter(|p| p.status == PeerStatus::Invited).count();
println!("Peers: {} active, {} invited", active, invited);
}
CommandResult::Ok
}
// --- Peer management ---
fn cmd_invite(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, args: &[String]) -> CommandResult {
let pubkey_hex = &args[0];
let pubkey: [u8; 32] = match hex::decode(pubkey_hex) {
Ok(bytes) if bytes.len() == 32 => bytes.try_into().unwrap(),
_ => {
eprintln!("Invalid pubkey: expected 64 hex chars (32 bytes)");
return CommandResult::Ok;
}
};
match block_async(node.invite_peer(&pubkey)) {
Ok(()) => {
println!("Invited peer: {}", pubkey_hex);
println!(" Status: {} (will become active after sync)", PeerStatus::Invited.as_str());
}
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
fn cmd_peers(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, _args: &[String]) -> CommandResult {
let peers = match block_async(node.list_peers()) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: {}", e);
return CommandResult::Ok;
}
};
if peers.is_empty() {
println!("No peers found.");
return CommandResult::Ok;
}
// Group peers by status
let mut by_status: std::collections::HashMap<PeerStatus, Vec<&lattice_core::PeerInfo>> =
std::collections::HashMap::new();
for peer in &peers {
by_status.entry(peer.status).or_default().push(peer);
}
// Print grouped by status in order: active, invited, dormant
let status_order = [PeerStatus::Active, PeerStatus::Invited, PeerStatus::Dormant];
for status in &status_order {
if let Some(peer_list) = by_status.get(status) {
println!("\n[{}] ({}):", status.as_str(), peer_list.len());
let mut sorted: Vec<_> = peer_list.iter().collect();
sorted.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
for peer in sorted {
let added_str = peer.added_at
.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_default();
let info_str = match (peer.name.as_ref(), added_str.is_empty()) {
(Some(name), false) => format!(" {} ({})", name, added_str),
(Some(name), true) => format!(" {}", name),
(None, false) => format!(" ({})", added_str),
(None, true) => String::new(),
};
println!(" {}{}", peer.pubkey, info_str);
}
}
}
CommandResult::Ok
}
fn cmd_remove(node: &Node, _store: Option<&StoreHandle>, _server: Option<&LatticeServer>, args: &[String]) -> CommandResult {
let pubkey_hex = &args[0];
let pubkey: [u8; 32] = match hex::decode(pubkey_hex) {
Ok(bytes) if bytes.len() == 32 => bytes.try_into().unwrap(),
_ => {
eprintln!("Invalid pubkey: expected 64 hex characters");
return CommandResult::Ok;
}
};
match block_async(node.remove_peer(&pubkey)) {
Ok(()) => println!("Removed peer: {}...", &pubkey_hex[..10]),
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
// --- Networking ---
fn cmd_join(_node: &Node, store: Option<&StoreHandle>, server: Option<&LatticeServer>, args: &[String]) -> CommandResult {
let server = match server {
Some(s) => s,
None => {
eprintln!("Iroh endpoint not started.");
return CommandResult::Ok;
}
};
if store.is_some() {
eprintln!("Already initialized. Use 'sync' to sync with peers.");
return CommandResult::Ok;
}
let peer_id = match lattice_net::parse_node_id(&args[0]) {
Ok(id) => id,
Err(e) => {
eprintln!("Invalid node ID: {}", e);
return CommandResult::Ok;
}
};
println!("Joining mesh via {}...", peer_id.fmt_short());
match block_async(server.join_mesh(peer_id)) {
Ok(handle) => {
println!("Joined mesh! Use 'sync' command to sync entries.");
CommandResult::SwitchTo(handle)
}
Err(e) => {
eprintln!("Join failed: {}", e);
CommandResult::Ok
}
}
}
fn cmd_sync(_node: &Node, store: Option<&StoreHandle>, server: Option<&LatticeServer>, args: &[String]) -> CommandResult {
let server = match server {
Some(s) => s,
None => {
eprintln!("Iroh endpoint not started.");
return CommandResult::Ok;
}
};
let store = match store {
Some(s) => s,
None => {
eprintln!("No store open. Use 'init' or 'join' first.");
return CommandResult::Ok;
}
};
if args.is_empty() {
// Sync with all active peers
match block_async(server.sync_all(store)) {
Ok(results) => {
if results.is_empty() {
println!("No peers to sync with.");
} else {
let total: u64 = results.iter().map(|r| r.entries_applied).sum();
println!("\nSync complete! Applied {} entries from {} peer(s).", total, results.len());
}
}
Err(e) => eprintln!("Sync failed: {}", e),
}
} else {
// Sync with specific peer
let peer_id = match lattice_net::parse_node_id(&args[0]) {
Ok(id) => id,
Err(e) => {
eprintln!("Invalid node ID: {}", e);
return CommandResult::Ok;
}
};
println!("Syncing with {}...", peer_id.fmt_short());
match block_async(server.sync_with_peer(store, peer_id)) {
Ok(result) => {
println!("Sync complete! Applied {} entries (peer sent {})",
result.entries_applied, result.entries_sent_by_peer);
}
Err(e) => eprintln!("Sync failed: {}", e),
}
}
CommandResult::Ok
}