use clap::{Arg, ArgMatches, Command};
use serde::Serialize;
use crate::command_add::installed::get_installed_components;
use crate::command_init::config::UiConfig;
use crate::command_init::workspace_utils::analyze_workspace;
use crate::shared::cli_error::CliResult;
const UI_CONFIG_TOML: &str = "ui_config.toml";
#[derive(Serialize)]
pub struct InfoData {
pub config_file: String,
pub base_color: String,
pub base_path: String,
pub workspace: Option<bool>,
pub target_crate: Option<String>,
pub installed: Vec<String>,
}
pub fn command_info() -> Command {
Command::new("info")
.about("Show project configuration and installed components")
.arg(
Arg::new("json")
.long("json")
.help("Output as JSON")
.action(clap::ArgAction::SetTrue),
)
}
pub fn process_info(matches: &ArgMatches) -> CliResult<()> {
let json = matches.get_flag("json");
let config = UiConfig::try_reading_ui_config(UI_CONFIG_TOML)?;
let installed = get_installed_components(&config.base_path_components);
let workspace = analyze_workspace().ok();
let data = build_info_data(&config.base_color, &config.base_path_components, &installed, workspace.as_ref());
let output = if json { format_info_json(&data)? } else { format_info(&data) };
println!("{output}");
Ok(())
}
pub fn build_info_data(
base_color: &str,
base_path: &str,
installed: &std::collections::HashSet<String>,
workspace: Option<&crate::command_init::workspace_utils::WorkspaceInfo>,
) -> InfoData {
let mut sorted_installed: Vec<String> = installed.iter().cloned().collect();
sorted_installed.sort();
let (ws_flag, target_crate) = match workspace {
Some(ws) => (Some(ws.is_workspace), ws.target_crate.clone()),
None => (None, None),
};
InfoData {
config_file: UI_CONFIG_TOML.to_string(),
base_color: base_color.to_string(),
base_path: base_path.to_string(),
workspace: ws_flag,
target_crate,
installed: sorted_installed,
}
}
pub fn format_info(data: &InfoData) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push(format!(" Config file {}", data.config_file));
lines.push(format!(" Base color {}", data.base_color));
lines.push(format!(" Base path {}", data.base_path));
if let Some(is_workspace) = data.workspace {
lines.push(format!(" Workspace {}", if is_workspace { "yes" } else { "no" }));
}
if let Some(ref crate_name) = data.target_crate {
lines.push(format!(" Target crate {crate_name}"));
}
let count = data.installed.len();
if count == 0 {
lines.push(" Installed none".to_string());
} else {
lines.push(format!(" Installed ({count}) {}", data.installed.join(", ")));
}
lines.join("\n")
}
pub fn format_info_json(data: &InfoData) -> CliResult<String> {
serde_json::to_string_pretty(data).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::command_init::workspace_utils::WorkspaceInfo;
fn installed(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
fn no_workspace() -> Option<WorkspaceInfo> {
None
}
fn single_crate_workspace() -> Option<WorkspaceInfo> {
Some(WorkspaceInfo {
is_workspace: false,
workspace_root: None,
target_crate: Some("my-app".to_string()),
target_crate_path: None,
components_base_path: "src/components".to_string(),
})
}
fn full_workspace() -> Option<WorkspaceInfo> {
Some(WorkspaceInfo {
is_workspace: true,
workspace_root: Some(std::path::PathBuf::from("/project")),
target_crate: Some("frontend".to_string()),
target_crate_path: None,
components_base_path: "frontend/src/components".to_string(),
})
}
fn data(color: &str, path: &str, names: &[&str], ws: Option<WorkspaceInfo>) -> InfoData {
build_info_data(color, path, &installed(names), ws.as_ref())
}
#[test]
fn shows_config_fields() {
let result = format_info(&data("neutral", "src/components", &[], no_workspace()));
assert!(result.contains("ui_config.toml"));
assert!(result.contains("neutral"));
assert!(result.contains("src/components"));
}
#[test]
fn shows_none_when_no_components_installed() {
let result = format_info(&data("neutral", "src/components", &[], no_workspace()));
assert!(result.contains("none"));
}
#[test]
fn shows_installed_components_sorted() {
let result = format_info(&data("neutral", "src/components", &["card", "button", "badge"], no_workspace()));
assert!(result.contains("badge, button, card"));
}
#[test]
fn shows_installed_count() {
let result = format_info(&data("neutral", "src/components", &["button", "badge"], no_workspace()));
assert!(result.contains("(2)"));
}
#[test]
fn shows_workspace_no_when_single_crate() {
let result = format_info(&data("neutral", "src/components", &[], single_crate_workspace()));
assert!(result.contains("no"));
}
#[test]
fn shows_workspace_yes_when_in_workspace() {
let result = format_info(&data("neutral", "src/components", &[], full_workspace()));
assert!(result.contains("yes"));
assert!(result.contains("frontend"));
}
#[test]
fn shows_target_crate_when_available() {
let result = format_info(&data("neutral", "src/components", &[], single_crate_workspace()));
assert!(result.contains("my-app"));
}
#[test]
fn no_workspace_info_omits_workspace_line() {
let result = format_info(&data("neutral", "src/components", &[], no_workspace()));
assert!(!result.contains("Workspace"));
assert!(!result.contains("Target crate"));
}
#[test]
fn single_installed_component() {
let result = format_info(&data("neutral", "src/components", &["button"], no_workspace()));
assert!(result.contains("(1)"));
assert!(result.contains("button"));
}
#[test]
fn json_output_is_valid_json() {
let d = data("neutral", "src/components", &["button"], no_workspace());
let json = format_info_json(&d).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_object());
}
#[test]
fn json_contains_all_fields() {
let d = data("neutral", "src/components", &["button"], no_workspace());
let json = format_info_json(&d).unwrap();
assert!(json.contains("base_color"));
assert!(json.contains("base_path"));
assert!(json.contains("config_file"));
assert!(json.contains("installed"));
}
#[test]
fn json_installed_is_array() {
let d = data("neutral", "src/components", &["badge", "button"], no_workspace());
let json = format_info_json(&d).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["installed"].is_array());
assert_eq!(parsed["installed"].as_array().unwrap().len(), 2);
}
#[test]
fn json_workspace_null_when_no_workspace() {
let d = data("neutral", "src/components", &[], no_workspace());
let json = format_info_json(&d).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["workspace"].is_null());
}
#[test]
fn json_workspace_true_when_in_workspace() {
let d = data("neutral", "src/components", &[], full_workspace());
let json = format_info_json(&d).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["workspace"], true);
}
#[test]
fn json_installed_sorted() {
let d = data("neutral", "src/components", &["card", "alert", "badge"], no_workspace());
let json = format_info_json(&d).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let names: Vec<&str> = parsed["installed"].as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
assert_eq!(names, vec!["alert", "badge", "card"]);
}
}