mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
719 lines
21 KiB
Rust
719 lines
21 KiB
Rust
#![allow(warnings)]
|
|
use anyhow::Result;
|
|
use clap::{Parser, Subcommand};
|
|
use comfy_table::{presets::UTF8_BORDERS_ONLY, Attribute, Cell, Table};
|
|
use sd_core::client::CoreClient;
|
|
use std::path::Path;
|
|
|
|
fn format_bytes(bytes: u64) -> String {
|
|
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
|
|
let mut size = bytes as f64;
|
|
let mut unit_index = 0;
|
|
|
|
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
|
size /= 1024.0;
|
|
unit_index += 1;
|
|
}
|
|
|
|
if unit_index == 0 {
|
|
format!("{} {}", bytes, UNITS[unit_index])
|
|
} else {
|
|
format!("{:.1} {}", size, UNITS[unit_index])
|
|
}
|
|
}
|
|
|
|
/// Validate instance name to prevent path traversal attacks
|
|
fn validate_instance_name(instance: &str) -> Result<(), String> {
|
|
if instance.is_empty() {
|
|
return Err("Instance name cannot be empty".to_string());
|
|
}
|
|
if instance.len() > 64 {
|
|
return Err("Instance name too long (max 64 characters)".to_string());
|
|
}
|
|
if !instance
|
|
.chars()
|
|
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
|
{
|
|
return Err("Instance name contains invalid characters. Only alphanumeric, dash, and underscore allowed".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
mod config;
|
|
mod context;
|
|
mod domains;
|
|
mod ui;
|
|
mod util;
|
|
|
|
use crate::context::{Context, OutputFormat};
|
|
use crate::domains::{
|
|
cloud, config as config_cmd,
|
|
daemon::{self, DaemonCmd},
|
|
devices::{self, DevicesCmd},
|
|
events::{self, EventsCmd},
|
|
file::{self, FileCmd},
|
|
index::{self, IndexCmd},
|
|
job::{self, JobCmd},
|
|
library::{self, LibraryCmd},
|
|
location::{self, LocationCmd},
|
|
logs::{self, LogsCmd},
|
|
network::{self, NetworkCmd},
|
|
search::{self, SearchCmd},
|
|
spaces::{self, SpacesCmd},
|
|
sync::{self, SyncCmd},
|
|
tag::{self, TagCmd},
|
|
update,
|
|
volume::{self, VolumeCmd},
|
|
};
|
|
|
|
use config_cmd::ConfigCmd;
|
|
|
|
// OutputFormat is defined in context.rs and shared across domains
|
|
|
|
/// Safely reset only Spacedrive v2 specific files and directories
|
|
/// This preserves any user data that might be in the data directory (like v1 backups)
|
|
fn reset_spacedrive_v2_data(data_dir: &Path) -> Result<()> {
|
|
let mut removed_items = Vec::new();
|
|
let mut errors = Vec::new();
|
|
|
|
// List of specific Spacedrive v2 files and directories to remove
|
|
let v2_items = [
|
|
"spacedrive.json", // Main app config
|
|
"device.json", // Device config
|
|
"libraries", // All v2 libraries
|
|
"logs", // Application logs
|
|
"job_logs", // Job logs
|
|
];
|
|
|
|
for item in &v2_items {
|
|
let path = data_dir.join(item);
|
|
if path.exists() {
|
|
let result = if path.is_dir() {
|
|
std::fs::remove_dir_all(&path)
|
|
} else {
|
|
std::fs::remove_file(&path)
|
|
};
|
|
|
|
match result {
|
|
Ok(()) => {
|
|
removed_items.push(item.to_string());
|
|
println!(" Removed: {}", item);
|
|
}
|
|
Err(e) => {
|
|
errors.push(format!("Failed to remove {}: {}", item, e));
|
|
println!(" Failed to remove {}: {}", item, e);
|
|
}
|
|
}
|
|
} else {
|
|
println!(" Not found: {}", item);
|
|
}
|
|
}
|
|
|
|
if !removed_items.is_empty() {
|
|
println!(
|
|
"Reset complete. Removed {} items: {}",
|
|
removed_items.len(),
|
|
removed_items.join(", ")
|
|
);
|
|
} else {
|
|
println!(" No Spacedrive v2 data found to reset.");
|
|
}
|
|
|
|
if !errors.is_empty() {
|
|
println!(" {} errors occurred during reset:", errors.len());
|
|
for error in &errors {
|
|
println!(" • {}", error);
|
|
}
|
|
// Don't fail the entire operation for partial cleanup failures
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "spacedrive", about = "Spacedrive v2 CLI (daemon client)")]
|
|
struct Cli {
|
|
/// Path to spacedrive data directory
|
|
#[arg(long)]
|
|
data_dir: Option<std::path::PathBuf>,
|
|
|
|
/// Daemon instance name
|
|
#[arg(long)]
|
|
instance: Option<String>,
|
|
|
|
/// Output format
|
|
#[arg(long, value_enum, default_value = "human")]
|
|
format: OutputFormat,
|
|
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum Commands {
|
|
/// Start the Spacedrive daemon
|
|
Start {
|
|
/// Run daemon in foreground (show logs)
|
|
#[arg(long)]
|
|
foreground: bool,
|
|
},
|
|
/// Stop the Spacedrive daemon
|
|
Stop {
|
|
/// Reset all data (requires confirmation)
|
|
#[arg(long)]
|
|
reset: bool,
|
|
},
|
|
/// Restart the Spacedrive daemon
|
|
Restart {
|
|
/// Run daemon in foreground after restart (show logs)
|
|
#[arg(long)]
|
|
foreground: bool,
|
|
/// Reset all data before restart (requires confirmation)
|
|
#[arg(long)]
|
|
reset: bool,
|
|
},
|
|
/// Core info
|
|
Status,
|
|
/// Configuration management
|
|
#[command(subcommand)]
|
|
Config(ConfigCmd),
|
|
/// Daemon management (auto-start, etc)
|
|
#[command(subcommand)]
|
|
Daemon(DaemonCmd),
|
|
/// Device operations (library database)
|
|
#[command(subcommand)]
|
|
Devices(DevicesCmd),
|
|
/// Monitor event bus in real-time
|
|
#[command(subcommand)]
|
|
Events(EventsCmd),
|
|
/// Libraries operations
|
|
#[command(subcommand)]
|
|
Library(LibraryCmd),
|
|
/// File operations
|
|
#[command(subcommand)]
|
|
File(FileCmd),
|
|
/// Indexing operations
|
|
#[command(subcommand)]
|
|
Index(IndexCmd),
|
|
/// Location operations
|
|
#[command(subcommand)]
|
|
Location(LocationCmd),
|
|
/// Networking and pairing
|
|
#[command(subcommand)]
|
|
Network(NetworkCmd),
|
|
/// Job commands
|
|
#[command(subcommand)]
|
|
Job(JobCmd),
|
|
/// View and follow logs
|
|
#[command(subcommand)]
|
|
Logs(LogsCmd),
|
|
/// Search operations
|
|
#[command(subcommand)]
|
|
Search(SearchCmd),
|
|
/// Spaces operations
|
|
#[command(subcommand)]
|
|
Spaces(SpacesCmd),
|
|
/// Sync operations and metrics
|
|
#[command(subcommand)]
|
|
Sync(SyncCmd),
|
|
/// Tag operations
|
|
#[command(subcommand)]
|
|
Tag(TagCmd),
|
|
/// Volume operations
|
|
#[command(subcommand)]
|
|
Volume(VolumeCmd),
|
|
/// Interactive cloud storage setup
|
|
Cloud,
|
|
/// Update CLI and daemon to latest version
|
|
Update {
|
|
/// Force update even if already on latest version
|
|
#[arg(long)]
|
|
force: bool,
|
|
},
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
let data_dir = cli.data_dir.unwrap_or(sd_core::config::default_data_dir()?);
|
|
let instance = cli.instance;
|
|
|
|
// Validate instance name for security
|
|
if let Some(ref inst) = instance {
|
|
validate_instance_name(inst)
|
|
.map_err(|e| anyhow::anyhow!("Invalid instance name: {}", e))?;
|
|
}
|
|
|
|
let socket_addr = if let Some(inst) = &instance {
|
|
let port = 6970 + (inst.bytes().map(|b| b as u16).sum::<u16>() % 1000);
|
|
format!("127.0.0.1:{}", port)
|
|
} else {
|
|
"127.0.0.1:6969".to_string()
|
|
};
|
|
|
|
match cli.command {
|
|
Commands::Start { foreground } => {
|
|
crate::ui::print_compact_logo();
|
|
println!("Starting daemon...");
|
|
|
|
// Check if daemon is already running
|
|
let client = CoreClient::new(socket_addr.clone());
|
|
match client
|
|
.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Ping)
|
|
.await
|
|
{
|
|
Ok(sd_core::infra::daemon::types::DaemonResponse::Pong) => {
|
|
println!("Daemon is already running");
|
|
return Ok(());
|
|
}
|
|
_ => {} // Daemon not running, continue
|
|
}
|
|
|
|
// Start daemon using std::process::Command
|
|
let current_exe = std::env::current_exe()?;
|
|
let daemon_path = current_exe.parent().unwrap().join("sd-daemon");
|
|
let mut command = std::process::Command::new(daemon_path);
|
|
|
|
// Pass data directory
|
|
command.arg("--data-dir").arg(&data_dir);
|
|
|
|
// Pass instance name if specified
|
|
if let Some(ref inst) = instance {
|
|
command.arg("--instance").arg(inst);
|
|
}
|
|
|
|
// Set working directory to current directory
|
|
command.current_dir(std::env::current_dir()?);
|
|
|
|
if foreground {
|
|
// Foreground mode: inherit stdout/stderr so logs are visible
|
|
println!("Starting daemon in foreground mode...");
|
|
println!("Press Ctrl+C to stop the daemon");
|
|
println!("═══════════════════════════════════════════════════════");
|
|
|
|
match command.status() {
|
|
Ok(status) => {
|
|
if status.success() {
|
|
println!("Daemon exited successfully");
|
|
} else {
|
|
return Err(anyhow::anyhow!("Daemon exited with error: {}", status));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow::anyhow!("Failed to start daemon: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
// Background mode: redirect stdout/stderr to null
|
|
command.stdout(std::process::Stdio::null());
|
|
command.stderr(std::process::Stdio::null());
|
|
|
|
match command.spawn() {
|
|
Ok(child) => {
|
|
println!("Daemon started (PID: {})", child.id());
|
|
|
|
// Wait a moment for daemon to start up
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
// Verify daemon is responding
|
|
match client
|
|
.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Ping)
|
|
.await
|
|
{
|
|
Ok(sd_core::infra::daemon::types::DaemonResponse::Pong) => {
|
|
println!("Daemon is ready and responding");
|
|
println!("Use 'sd logs follow' to view daemon logs");
|
|
}
|
|
_ => {
|
|
println!("Warning: Daemon may not be fully initialized yet");
|
|
println!("Use 'sd logs follow' to check daemon status");
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow::anyhow!("Failed to start daemon: {}", e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Commands::Stop { reset } => {
|
|
if reset {
|
|
use crate::util::confirm::confirm_or_abort;
|
|
confirm_or_abort(
|
|
" This will permanently delete Spacedrive v2 data (libraries, settings, logs). Other files in the data directory will be preserved. Are you sure?",
|
|
false
|
|
)?;
|
|
}
|
|
|
|
println!("Stopping daemon...");
|
|
let core = CoreClient::new(socket_addr.clone());
|
|
let stop_result = core
|
|
.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Shutdown)
|
|
.await;
|
|
|
|
match stop_result {
|
|
Ok(_) => {
|
|
println!("Daemon shutdown initiated.");
|
|
println!("Note: If jobs are running, the daemon will wait for them to pause before fully shutting down.");
|
|
println!("Use 'sd logs follow' to monitor shutdown progress.");
|
|
}
|
|
Err(_) => {
|
|
if reset {
|
|
println!(" Daemon was not running, proceeding with reset...");
|
|
} else {
|
|
println!(" Daemon was not running or already stopped.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if reset {
|
|
println!("Resetting Spacedrive v2 data...");
|
|
reset_spacedrive_v2_data(&data_dir)?;
|
|
}
|
|
}
|
|
Commands::Restart { foreground, reset } => {
|
|
if reset {
|
|
use crate::util::confirm::confirm_or_abort;
|
|
confirm_or_abort(
|
|
" This will permanently delete Spacedrive v2 data (libraries, settings, logs) before restart. Other files in the data directory will be preserved. Are you sure?",
|
|
false
|
|
)?;
|
|
}
|
|
|
|
// First, try to stop the daemon if it's running
|
|
println!("Stopping daemon...");
|
|
let core = CoreClient::new(socket_addr.clone());
|
|
let stop_result = core
|
|
.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Shutdown)
|
|
.await;
|
|
|
|
match stop_result {
|
|
Ok(_) => {
|
|
println!("Daemon shutdown initiated.");
|
|
println!("Waiting for daemon to fully shut down before restart...");
|
|
// Give some time for shutdown to complete
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
}
|
|
Err(_) => println!(" Daemon was not running or already stopped."),
|
|
}
|
|
|
|
// Reset data if requested
|
|
if reset {
|
|
println!("Resetting Spacedrive v2 data...");
|
|
reset_spacedrive_v2_data(&data_dir)?;
|
|
}
|
|
|
|
// Wait a moment for cleanup
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
|
|
// Start the daemon again
|
|
println!("Starting daemon...");
|
|
let current_exe = std::env::current_exe()?;
|
|
let daemon_path = current_exe.parent().unwrap().join("sd-daemon");
|
|
let mut cmd = std::process::Command::new(daemon_path);
|
|
|
|
// Pass data directory
|
|
cmd.arg("--data-dir").arg(&data_dir);
|
|
|
|
// Pass instance name if specified
|
|
if let Some(ref inst) = instance {
|
|
cmd.arg("--instance").arg(inst);
|
|
}
|
|
|
|
// Set working directory to current directory
|
|
cmd.current_dir(std::env::current_dir()?);
|
|
|
|
if foreground {
|
|
// Foreground mode: inherit stdout/stderr so logs are visible
|
|
println!("Starting daemon in foreground mode...");
|
|
println!("Press Ctrl+C to stop the daemon");
|
|
println!("═══════════════════════════════════════════════════════");
|
|
|
|
match cmd.status() {
|
|
Ok(status) => {
|
|
if status.success() {
|
|
println!("Daemon exited successfully");
|
|
} else {
|
|
return Err(anyhow::anyhow!("Daemon exited with error: {}", status));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow::anyhow!("Failed to start daemon: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
// Run in background
|
|
cmd.stdout(std::process::Stdio::null());
|
|
cmd.stderr(std::process::Stdio::null());
|
|
|
|
let child = cmd.spawn()?;
|
|
println!("Daemon restarted (PID: {})", child.id());
|
|
|
|
// Wait a moment and check if it's still running
|
|
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
|
|
|
// Try to connect to verify it started successfully
|
|
let core = CoreClient::new(socket_addr.clone());
|
|
match core
|
|
.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Ping)
|
|
.await
|
|
{
|
|
Ok(_) => println!("Daemon restart successful"),
|
|
Err(e) => {
|
|
println!(" Warning: Could not verify daemon status: {}", e);
|
|
println!("Use 'sd status' to check daemon status");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Commands::Config(cmd) => {
|
|
// Config management doesn't need the client
|
|
config_cmd::run(data_dir, cmd).await?;
|
|
}
|
|
Commands::Daemon(cmd) => {
|
|
// Daemon management doesn't need the client, handle directly
|
|
daemon::run(data_dir, instance, cmd).await?;
|
|
}
|
|
Commands::Update { force } => {
|
|
// Update doesn't need the client
|
|
update::run(data_dir, force).await?;
|
|
}
|
|
_ => {
|
|
run_client_command(cli.command, cli.format, data_dir, socket_addr).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn run_client_command(
|
|
command: Commands,
|
|
format: OutputFormat,
|
|
data_dir: std::path::PathBuf,
|
|
socket_addr: String,
|
|
) -> Result<()> {
|
|
// Initialize device ID and slug from device.json if it exists
|
|
if let Ok(device_config) = std::fs::read_to_string(data_dir.join("device.json")) {
|
|
if let Ok(device_json) = serde_json::from_str::<serde_json::Value>(&device_config) {
|
|
if let Some(device_id_str) = device_json.get("id").and_then(|v| v.as_str()) {
|
|
if let Ok(device_id) = uuid::Uuid::parse_str(device_id_str) {
|
|
sd_core::device::set_current_device_id(device_id);
|
|
}
|
|
}
|
|
if let Some(device_slug) = device_json.get("slug").and_then(|v| v.as_str()) {
|
|
sd_core::device::set_current_device_slug(device_slug.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
let core = CoreClient::new(socket_addr.clone());
|
|
let mut ctx = Context::new(core, format, data_dir, socket_addr)?;
|
|
|
|
ctx.validate_and_fix_library().await?;
|
|
|
|
match command {
|
|
Commands::Status => {
|
|
let status: sd_core::ops::core::status::output::CoreStatus =
|
|
execute_core_query!(ctx, ());
|
|
match ctx.format {
|
|
OutputFormat::Human => {
|
|
// Display logo
|
|
crate::ui::logo::print_logo_colored();
|
|
println!();
|
|
|
|
// Device Information
|
|
let mut device_table = Table::new();
|
|
device_table.load_preset(UTF8_BORDERS_ONLY);
|
|
device_table.set_header(vec![
|
|
Cell::new("Device Information").add_attribute(Attribute::Bold),
|
|
Cell::new(""),
|
|
]);
|
|
device_table.add_row(vec!["Name", &status.device_info.name]);
|
|
device_table.add_row(vec!["ID", &status.device_info.id.to_string()]);
|
|
device_table.add_row(vec!["OS", &status.device_info.os]);
|
|
if let Some(model) = &status.device_info.hardware_model {
|
|
device_table.add_row(vec!["Hardware", model]);
|
|
}
|
|
device_table.add_row(vec![
|
|
"Created",
|
|
&status
|
|
.device_info
|
|
.created_at
|
|
.format("%Y-%m-%d %H:%M:%S UTC")
|
|
.to_string(),
|
|
]);
|
|
println!("{}", device_table);
|
|
println!();
|
|
|
|
// System Status
|
|
let mut system_table = Table::new();
|
|
system_table.load_preset(UTF8_BORDERS_ONLY);
|
|
system_table.set_header(vec![
|
|
Cell::new("System Status").add_attribute(Attribute::Bold),
|
|
Cell::new(""),
|
|
]);
|
|
system_table.add_row(vec!["Core Version", &status.version]);
|
|
system_table.add_row(vec!["Built At", &status.built_at]);
|
|
system_table.add_row(vec!["Data Directory", &status.system.data_directory]);
|
|
if let Some(instance) = &status.system.instance_name {
|
|
system_table.add_row(vec!["Instance", instance]);
|
|
}
|
|
if let Some(current_lib) = &status.system.current_library {
|
|
system_table.add_row(vec!["Current Library", current_lib]);
|
|
} else {
|
|
system_table.add_row(vec!["Current Library", "None"]);
|
|
}
|
|
if let Some(uptime) = status.system.uptime {
|
|
let hours = uptime / 3600;
|
|
let minutes = (uptime % 3600) / 60;
|
|
system_table.add_row(vec!["Uptime", &format!("{}h {}m", hours, minutes)]);
|
|
}
|
|
system_table.add_row(vec!["Status", "● Running"]);
|
|
println!("{}", system_table);
|
|
println!();
|
|
|
|
// Libraries
|
|
let mut libraries_table = Table::new();
|
|
libraries_table.load_preset(UTF8_BORDERS_ONLY);
|
|
libraries_table.set_header(vec![
|
|
Cell::new(format!("Libraries ({})", status.library_count))
|
|
.add_attribute(Attribute::Bold),
|
|
Cell::new(""),
|
|
]);
|
|
if status.libraries.is_empty() {
|
|
libraries_table
|
|
.add_row(vec!["No libraries found".to_string(), "".to_string()]);
|
|
} else {
|
|
for lib in &status.libraries {
|
|
let lib_name = format!("● {}", lib.name);
|
|
libraries_table.add_row(vec![lib_name, lib.id.to_string()]);
|
|
libraries_table.add_row(vec![
|
|
format!(" Path: {}", lib.path.display()),
|
|
"".to_string(),
|
|
]);
|
|
if let Some(stats) = &lib.stats {
|
|
libraries_table.add_row(vec![
|
|
format!(" Files: {}", stats.total_files),
|
|
"".to_string(),
|
|
]);
|
|
libraries_table.add_row(vec![
|
|
format!(" Size: {}", format_bytes(stats.total_size)),
|
|
"".to_string(),
|
|
]);
|
|
libraries_table.add_row(vec![
|
|
format!(" Locations: {}", stats.location_count),
|
|
"".to_string(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
println!("{}", libraries_table);
|
|
println!();
|
|
|
|
// Services
|
|
let mut services_table = Table::new();
|
|
services_table.load_preset(UTF8_BORDERS_ONLY);
|
|
services_table.set_header(vec![
|
|
Cell::new("Services").add_attribute(Attribute::Bold),
|
|
Cell::new(""),
|
|
]);
|
|
let services = &status.services;
|
|
|
|
let watcher_status = if services.location_watcher.running {
|
|
"● Running"
|
|
} else {
|
|
"○ Stopped"
|
|
};
|
|
services_table.add_row(vec!["Location Watcher", watcher_status]);
|
|
|
|
let net_status = if services.networking.running {
|
|
"● Running"
|
|
} else {
|
|
"○ Stopped"
|
|
};
|
|
services_table.add_row(vec!["Networking", net_status]);
|
|
|
|
let vol_status = if services.volume_monitor.running {
|
|
"● Running"
|
|
} else {
|
|
"○ Stopped"
|
|
};
|
|
services_table.add_row(vec!["Volume Monitor", vol_status]);
|
|
|
|
let share_status = if services.file_sharing.running {
|
|
"● Running"
|
|
} else {
|
|
"○ Stopped"
|
|
};
|
|
services_table.add_row(vec!["File Sharing", share_status]);
|
|
println!("{}", services_table);
|
|
println!();
|
|
|
|
// Network
|
|
let mut network_table = Table::new();
|
|
network_table.load_preset(UTF8_BORDERS_ONLY);
|
|
network_table.set_header(vec![
|
|
Cell::new("Network").add_attribute(Attribute::Bold),
|
|
Cell::new(""),
|
|
]);
|
|
if status.network.running {
|
|
network_table.add_row(vec!["Status", "● Running"]);
|
|
|
|
if let Some(node_id) = &status.network.node_id {
|
|
let node_id_display = if node_id.len() > 50 {
|
|
format!("{}...", &node_id[..47])
|
|
} else {
|
|
node_id.clone()
|
|
};
|
|
network_table.add_row(vec!["Node ID", &node_id_display]);
|
|
}
|
|
|
|
network_table.add_row(vec![
|
|
"Connected Devices",
|
|
&status.network.connected_devices.to_string(),
|
|
]);
|
|
|
|
network_table.add_row(vec![
|
|
"Paired Devices",
|
|
&status.network.paired_devices.to_string(),
|
|
]);
|
|
|
|
if !status.network.addresses.is_empty() {
|
|
network_table.add_row(vec![
|
|
"Addresses",
|
|
&format!("{} address(es)", status.network.addresses.len()),
|
|
]);
|
|
for addr in &status.network.addresses {
|
|
network_table.add_row(vec![format!(" {}", addr), "".to_string()]);
|
|
}
|
|
}
|
|
|
|
network_table.add_row(vec!["Version", &status.network.version]);
|
|
} else {
|
|
network_table.add_row(vec!["Status", "○ Stopped"]);
|
|
}
|
|
println!("{}", network_table);
|
|
}
|
|
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&status)?),
|
|
}
|
|
}
|
|
Commands::Devices(cmd) => devices::run(&ctx, cmd).await?,
|
|
Commands::Events(cmd) => events::run(&ctx, cmd).await?,
|
|
Commands::Library(cmd) => library::run(&ctx, cmd).await?,
|
|
Commands::File(cmd) => file::run(&ctx, cmd).await?,
|
|
Commands::Index(cmd) => index::run(&ctx, cmd).await?,
|
|
Commands::Location(cmd) => location::run(&ctx, cmd).await?,
|
|
Commands::Network(cmd) => network::run(&ctx, cmd).await?,
|
|
Commands::Job(cmd) => job::run(&ctx, cmd).await?,
|
|
Commands::Sync(cmd) => sync::run(&ctx, cmd).await?,
|
|
Commands::Logs(cmd) => logs::run(&ctx, cmd).await?,
|
|
Commands::Search(cmd) => search::run(&ctx, cmd).await?,
|
|
Commands::Spaces(cmd) => spaces::exec(cmd, &ctx).await?,
|
|
Commands::Tag(cmd) => tag::run(&ctx, cmd).await?,
|
|
Commands::Volume(cmd) => volume::run(&ctx, cmd).await?,
|
|
Commands::Cloud => cloud::run(&ctx).await?,
|
|
_ => {} // Start and Stop are handled in main
|
|
}
|
|
Ok(())
|
|
}
|