| 1 |
use clap::Args; |
| 2 |
use anyhow::{Context, Result}; |
| 3 |
use dialoguer::{theme::ColorfulTheme, Input, Confirm}; |
| 4 |
use std::path::PathBuf; |
| 5 |
use tracing::{info, warn}; |
| 6 |
|
| 7 |
use crate::config::Config; |
| 8 |
use crate::client::ZephyrClient; |
| 9 |
use super::Command; |
| 10 |
|
| 11 |
#[derive(Debug, Args)] |
| 12 |
pub struct InitCommand { |
| 13 |
/// Node name (optional) |
| 14 |
#[arg(short, long)] |
| 15 |
name: Option<String>, |
| 16 |
|
| 17 |
/// Data directory path |
| 18 |
#[arg(short, long)] |
| 19 |
data_dir: Option<PathBuf>, |
| 20 |
|
| 21 |
/// Listen port for the node |
| 22 |
#[arg(short, long)] |
| 23 |
port: Option<u16>, |
| 24 |
|
| 25 |
/// Maximum storage allocation in GB |
| 26 |
#[arg(short, long)] |
| 27 |
storage: Option<u64>, |
| 28 |
|
| 29 |
/// Skip interactive configuration |
| 30 |
#[arg(long)] |
| 31 |
no_interactive: bool, |
| 32 |
} |
| 33 |
|
| 34 |
#[async_trait::async_trait] |
| 35 |
impl Command for InitCommand { |
| 36 |
async fn execute(&self, config: &Config) -> Result<()> { |
| 37 |
info!("Initializing ZephyrFS node..."); |
| 38 |
|
| 39 |
let mut node_config = config.clone(); |
| 40 |
|
| 41 |
// Interactive configuration if not skipped |
| 42 |
if !self.no_interactive { |
| 43 |
self.interactive_config(&mut node_config)?; |
| 44 |
} else { |
| 45 |
self.apply_args(&mut node_config)?; |
| 46 |
} |
| 47 |
|
| 48 |
// Ensure data directory exists |
| 49 |
node_config.ensure_data_dir() |
| 50 |
.context("Failed to create data directory")?; |
| 51 |
|
| 52 |
// Save configuration |
| 53 |
node_config.save(None) |
| 54 |
.context("Failed to save configuration")?; |
| 55 |
|
| 56 |
// Initialize the node |
| 57 |
let client = ZephyrClient::new(&node_config); |
| 58 |
client.initialize_node().await |
| 59 |
.context("Failed to initialize node")?; |
| 60 |
|
| 61 |
println!("✓ ZephyrFS node initialized successfully!"); |
| 62 |
println!(" Node ID: {}", node_config.node.id.unwrap_or_else(|| "auto-generated".to_string())); |
| 63 |
println!(" Data directory: {:?}", node_config.node.data_dir); |
| 64 |
println!(" Listen port: {}", node_config.node.listen_port); |
| 65 |
println!(" Max storage: {} GB", node_config.storage.max_storage_gb); |
| 66 |
|
| 67 |
println!("\nNext steps:"); |
| 68 |
println!(" 1. Start the node: zephyrfs status"); |
| 69 |
println!(" 2. Join a network: zephyrfs join <bootstrap-peer>"); |
| 70 |
|
| 71 |
Ok(()) |
| 72 |
} |
| 73 |
} |
| 74 |
|
| 75 |
impl InitCommand { |
| 76 |
fn interactive_config(&self, config: &mut Config) -> Result<()> { |
| 77 |
let theme = ColorfulTheme::default(); |
| 78 |
|
| 79 |
// Node name |
| 80 |
if let Some(name) = &self.name { |
| 81 |
config.node.name = Some(name.clone()); |
| 82 |
} else { |
| 83 |
let name: String = Input::with_theme(&theme) |
| 84 |
.with_prompt("Node name (optional)") |
| 85 |
.default(config.node.name.clone().unwrap_or_else(|| "zephyr-node".to_string())) |
| 86 |
.allow_empty(true) |
| 87 |
.interact_text()?; |
| 88 |
|
| 89 |
config.node.name = if name.is_empty() { None } else { Some(name) }; |
| 90 |
} |
| 91 |
|
| 92 |
// Data directory |
| 93 |
if let Some(data_dir) = &self.data_dir { |
| 94 |
config.node.data_dir = data_dir.clone(); |
| 95 |
} else { |
| 96 |
let data_dir_str = Input::with_theme(&theme) |
| 97 |
.with_prompt("Data directory") |
| 98 |
.default(config.node.data_dir.to_string_lossy().to_string()) |
| 99 |
.interact_text()?; |
| 100 |
|
| 101 |
config.node.data_dir = PathBuf::from(data_dir_str); |
| 102 |
} |
| 103 |
|
| 104 |
// Listen port |
| 105 |
if let Some(port) = self.port { |
| 106 |
config.node.listen_port = port; |
| 107 |
} else { |
| 108 |
config.node.listen_port = Input::with_theme(&theme) |
| 109 |
.with_prompt("Listen port") |
| 110 |
.default(config.node.listen_port) |
| 111 |
.interact()?; |
| 112 |
} |
| 113 |
|
| 114 |
// Storage allocation |
| 115 |
if let Some(storage) = self.storage { |
| 116 |
config.storage.max_storage_gb = storage; |
| 117 |
} else { |
| 118 |
config.storage.max_storage_gb = Input::with_theme(&theme) |
| 119 |
.with_prompt("Maximum storage allocation (GB)") |
| 120 |
.default(config.storage.max_storage_gb) |
| 121 |
.interact()?; |
| 122 |
} |
| 123 |
|
| 124 |
// Confirm settings |
| 125 |
println!("\nConfiguration summary:"); |
| 126 |
println!(" Name: {}", config.node.name.as_deref().unwrap_or("auto-generated")); |
| 127 |
println!(" Data directory: {:?}", config.node.data_dir); |
| 128 |
println!(" Listen port: {}", config.node.listen_port); |
| 129 |
println!(" Max storage: {} GB", config.storage.max_storage_gb); |
| 130 |
|
| 131 |
let confirm = Confirm::with_theme(&theme) |
| 132 |
.with_prompt("Proceed with initialization?") |
| 133 |
.default(true) |
| 134 |
.interact()?; |
| 135 |
|
| 136 |
if !confirm { |
| 137 |
warn!("Initialization cancelled by user"); |
| 138 |
std::process::exit(0); |
| 139 |
} |
| 140 |
|
| 141 |
Ok(()) |
| 142 |
} |
| 143 |
|
| 144 |
fn apply_args(&self, config: &mut Config) -> Result<()> { |
| 145 |
if let Some(name) = &self.name { |
| 146 |
config.node.name = Some(name.clone()); |
| 147 |
} |
| 148 |
|
| 149 |
if let Some(data_dir) = &self.data_dir { |
| 150 |
config.node.data_dir = data_dir.clone(); |
| 151 |
} |
| 152 |
|
| 153 |
if let Some(port) = self.port { |
| 154 |
config.node.listen_port = port; |
| 155 |
} |
| 156 |
|
| 157 |
if let Some(storage) = self.storage { |
| 158 |
config.storage.max_storage_gb = storage; |
| 159 |
} |
| 160 |
|
| 161 |
Ok(()) |
| 162 |
} |
| 163 |
} |