ztnet 0.1.14

ZTNet CLI — manage ZeroTier networks via ZTNet
use std::io::{self, Read, Write};
use std::path::PathBuf;

use serde_json::Value;

use crate::cli::{GlobalOpts, OutputFormat};
use crate::config::{self, Config};
use crate::error::CliError;
use crate::output;

pub(super) fn confirm(global: &GlobalOpts, prompt: &str) -> Result<bool, CliError> {
	if global.dry_run {
		return Ok(true);
	}
	if global.yes {
		return Ok(true);
	}
	if global.quiet {
		return Err(CliError::InvalidArgument(
			"refusing to prompt in --quiet mode (pass --yes)".to_string(),
		));
	}

	eprint!("{prompt}[y/N]: ");
	io::stderr().flush()?;

	let mut input = String::new();
	io::stdin().read_line(&mut input)?;
	let input = input.trim().to_ascii_lowercase();
	Ok(matches!(input.as_str(), "y" | "yes"))
}

pub(super) fn write_text_output(
	out: &str,
	path: Option<&PathBuf>,
	global: &GlobalOpts,
) -> Result<(), CliError> {
	if let Some(path) = path {
		if let Some(parent) = path.parent() {
			std::fs::create_dir_all(parent)?;
		}
		std::fs::write(path, out)?;
		if !global.quiet {
			eprintln!("Wrote {} bytes to {}.", out.as_bytes().len(), path.display());
		}
		return Ok(());
	}

	print!("{out}");
	Ok(())
}

pub(super) fn load_config_store() -> Result<(PathBuf, Config), CliError> {
	let config_path = config::default_config_path()?;
	let cfg = config::load_config(&config_path)?;
	Ok((config_path, cfg))
}

pub(super) fn read_stdin_trimmed() -> Result<String, CliError> {
	let mut input = String::new();
	io::stdin().read_to_string(&mut input)?;
	Ok(input.trim().to_string())
}

pub(super) fn print_human_or_machine(
	value: &Value,
	format: OutputFormat,
	no_color: bool,
) -> Result<(), CliError> {
	if matches!(format, OutputFormat::Table) {
		print_kv(value);
		return Ok(());
	}
	output::print_value(value, format, no_color)
}

pub(super) fn print_kv(value: &Value) {
	let Some(obj) = value.as_object() else {
		println!("{value}");
		return;
	};

	let mut keys: Vec<&String> = obj.keys().collect();
	keys.sort();
	for key in keys {
		let v = &obj[key];
		println!("{key}: {}", render_scalar(v));
	}
}

pub(super) fn render_scalar(value: &Value) -> String {
	match value {
		Value::Null => String::new(),
		Value::Bool(v) => v.to_string(),
		Value::Number(v) => v.to_string(),
		Value::String(v) => v.clone(),
		_ => value.to_string(),
	}
}

pub(super) fn opt_string(value: Option<String>) -> Value {
	value.map(Value::String).unwrap_or(Value::Null)
}

pub(super) fn redact_token(token: &str) -> String {
	const KEEP: usize = 4;
	if token.len() <= KEEP * 2 {
		return "REDACTED".to_string();
	}
	format!("{}{}", &token[..KEEP], &token[token.len() - KEEP..])
}