use super::definition::RolesConfig;
use super::resolver::{RoleDiff, RoleResolver, diff_roles};
use clap::Subcommand;
use std::collections::HashMap;
#[derive(Clone, Subcommand)]
pub enum RolesAction {
List {
#[clap(short, long)]
verbose: bool,
#[clap(short = 'F', long = "format", default_value = "pretty")]
output_format: String,
},
Show {
name: String,
#[clap(long)]
inheritance: bool,
#[clap(long)]
resolved: bool,
#[clap(short = 'F', long = "format", default_value = "pretty")]
output_format: String,
},
Diff {
role_a: String,
role_b: Option<String>,
#[clap(long)]
current: bool,
#[clap(short = 'F', long = "format", default_value = "pretty")]
output_format: String,
},
}
pub fn handle_roles_command(
action: RolesAction,
roles_config: Option<&RolesConfig>,
current_role: Option<&str>,
) -> Result<(), String> {
let config = roles_config.ok_or("No roles defined in configuration")?;
if config.roles.is_empty() {
println!("No roles defined in configuration.");
println!("\nTo define roles, add [roles.name] sections to your jarvy.toml:");
println!();
println!(" [roles.frontend]");
println!(" description = \"Frontend development stack\"");
println!(" tools = [\"node\", \"bun\", \"pnpm\"]");
println!();
println!(" [roles.frontend.tools]");
println!(" node = \"20\"");
return Ok(());
}
match action {
RolesAction::List {
verbose,
output_format,
} => handle_list(config, verbose, &output_format),
RolesAction::Show {
name,
inheritance,
resolved,
output_format,
} => handle_show(config, &name, inheritance, resolved, &output_format),
RolesAction::Diff {
role_a,
role_b,
current,
output_format,
} => handle_diff(
config,
&role_a,
role_b.as_deref(),
current,
current_role,
&output_format,
),
}
}
fn handle_list(config: &RolesConfig, verbose: bool, output_format: &str) -> Result<(), String> {
let resolver = RoleResolver::new(config);
let mut roles: Vec<_> = resolver.list_roles().into_iter().collect();
roles.sort();
if output_format == "json" {
let json_output: Vec<_> = roles
.iter()
.map(|name| {
let role = resolver.get_role(name).unwrap();
let mut obj = serde_json::json!({
"name": name,
"description": role.description(),
"extends": if role.has_extends() {
Some(role.get_extends())
} else {
None
},
});
if verbose {
let def = role.clone().into_definition();
obj["tool_count"] = serde_json::json!(def.tool_count());
}
obj
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
} else {
println!("Available roles:\n");
for name in &roles {
let role = resolver.get_role(name).unwrap();
let desc = role
.description()
.map(|d| format!(" - {}", d))
.unwrap_or_default();
let extends_info = if role.has_extends() {
format!(" (extends: {})", role.get_extends().join(", "))
} else {
String::new()
};
if verbose {
let def = role.clone().into_definition();
println!(
" {} ({} tools){}\n {}",
name,
def.tool_count(),
extends_info,
if desc.is_empty() {
"(no description)".to_string()
} else {
desc.trim_start_matches(" - ").to_string()
}
);
} else {
println!(" {}{}{}", name, extends_info, desc);
}
}
println!();
println!("Use 'jarvy roles show <name>' for details on a specific role.");
}
Ok(())
}
fn handle_show(
config: &RolesConfig,
name: &str,
show_inheritance: bool,
show_resolved: bool,
output_format: &str,
) -> Result<(), String> {
let mut resolver = RoleResolver::new(config);
let role_def = resolver
.get_role(name)
.ok_or_else(|| format!("Role '{}' not found", name))?
.clone();
let resolved = resolver.resolve(name).map_err(|e| e.to_string())?;
if output_format == "json" {
let def = role_def.clone().into_definition();
let mut json_output = serde_json::json!({
"name": name,
"description": resolved.description,
"extends": if role_def.has_extends() {
Some(role_def.get_extends())
} else {
None
},
"direct_tools": def.get_tools(),
});
if show_inheritance || show_resolved {
json_output["inheritance_chain"] = serde_json::json!(resolved.inheritance_chain);
}
if show_resolved {
let resolved_tools: HashMap<_, _> = resolved
.tools
.iter()
.map(|(k, v)| {
(
k.clone(),
serde_json::json!({
"version": v.version,
"source": v.source_role,
}),
)
})
.collect();
json_output["resolved_tools"] = serde_json::json!(resolved_tools);
}
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
} else {
println!("Role: {}\n", name);
if let Some(desc) = &resolved.description {
println!(" Description: {}\n", desc);
}
if role_def.has_extends() {
println!(" Extends: {}\n", role_def.get_extends().join(", "));
}
if show_inheritance {
println!(
" Inheritance chain: {}\n",
resolved.inheritance_chain.join(" -> ")
);
}
let def = role_def.clone().into_definition();
let direct_tools = def.get_tools();
if !direct_tools.is_empty() {
println!(" Direct tools ({}):", direct_tools.len());
let mut tools: Vec<_> = direct_tools.iter().collect();
tools.sort_by_key(|(name, _)| *name);
for (tool_name, version) in tools {
println!(" {} = {}", tool_name, version);
}
println!();
}
if show_resolved {
println!(" Resolved tools ({} total):", resolved.tools.len());
let mut tools: Vec<_> = resolved.tools.iter().collect();
tools.sort_by_key(|(name, _)| *name);
for (tool_name, tool) in tools {
let source = if tool.source_role != name {
format!(" (from {})", tool.source_role)
} else {
String::new()
};
println!(" {} = {}{}", tool_name, tool.version, source);
}
}
}
Ok(())
}
fn handle_diff(
config: &RolesConfig,
role_a: &str,
role_b: Option<&str>,
use_current: bool,
current_role: Option<&str>,
output_format: &str,
) -> Result<(), String> {
let mut resolver = RoleResolver::new(config);
let role_b_name = if use_current {
current_role.ok_or("No role currently assigned. Use 'role = \"name\"' in jarvy.toml")?
} else {
role_b.ok_or("Please specify a second role or use --current")?
};
let resolved_a = resolver.resolve(role_a).map_err(|e| e.to_string())?;
let resolved_b = resolver.resolve(role_b_name).map_err(|e| e.to_string())?;
let diff = diff_roles(&resolved_a, &resolved_b);
if output_format == "json" {
let json_output = serde_json::json!({
"role_a": diff.role_a,
"role_b": diff.role_b,
"is_identical": diff.is_identical(),
"only_in_a": diff.only_in_a,
"only_in_b": diff.only_in_b,
"different_versions": diff.different_versions,
"same": diff.same,
});
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
} else {
print_diff_pretty(&diff);
}
Ok(())
}
fn print_diff_pretty(diff: &RoleDiff) {
println!("Comparing roles: {} vs {}\n", diff.role_a, diff.role_b);
if diff.is_identical() {
println!(" Roles are identical.\n");
return;
}
if !diff.only_in_a.is_empty() {
println!(" Only in {}:", diff.role_a);
let mut tools: Vec<_> = diff.only_in_a.iter().collect();
tools.sort_by_key(|(name, _)| *name);
for (name, version) in tools {
println!(" - {} = {}", name, version);
}
println!();
}
if !diff.only_in_b.is_empty() {
println!(" Only in {}:", diff.role_b);
let mut tools: Vec<_> = diff.only_in_b.iter().collect();
tools.sort_by_key(|(name, _)| *name);
for (name, version) in tools {
println!(" + {} = {}", name, version);
}
println!();
}
if !diff.different_versions.is_empty() {
println!(" Different versions:");
let mut tools: Vec<_> = diff.different_versions.iter().collect();
tools.sort_by_key(|(name, _)| *name);
for (name, (ver_a, ver_b)) in tools {
println!(" {} : {} -> {}", name, ver_a, ver_b);
}
println!();
}
if !diff.same.is_empty() {
println!(" Same in both ({} tools):", diff.same.len());
let mut tools: Vec<_> = diff.same.keys().collect();
tools.sort();
let display: Vec<_> = tools.iter().take(5).map(|s| s.as_str()).collect();
let remaining = tools.len().saturating_sub(5);
print!(" {}", display.join(", "));
if remaining > 0 {
print!(" (+{} more)", remaining);
}
println!("\n");
}
println!(
"Summary: {} differences ({} only in {}, {} only in {}, {} version differences)",
diff.difference_count(),
diff.only_in_a.len(),
diff.role_a,
diff.only_in_b.len(),
diff.role_b,
diff.different_versions.len()
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::roles::definition::{RoleDefinition, RoleDefinitionWrapper};
fn create_test_config() -> RolesConfig {
let mut roles = HashMap::new();
let frontend = RoleDefinition {
description: Some("Frontend tools".to_string()),
tools: vec!["node".to_string(), "bun".to_string()],
..Default::default()
};
roles.insert(
"frontend".to_string(),
RoleDefinitionWrapper::Simple(frontend),
);
let backend = RoleDefinition {
description: Some("Backend tools".to_string()),
tools: vec!["rust".to_string(), "go".to_string()],
..Default::default()
};
roles.insert(
"backend".to_string(),
RoleDefinitionWrapper::Simple(backend),
);
RolesConfig { roles }
}
#[test]
fn test_handle_list() {
let config = create_test_config();
let result = handle_list(&config, false, "pretty");
assert!(result.is_ok());
}
#[test]
fn test_handle_show() {
let config = create_test_config();
let result = handle_show(&config, "frontend", false, false, "pretty");
assert!(result.is_ok());
}
#[test]
fn test_handle_show_nonexistent() {
let config = create_test_config();
let result = handle_show(&config, "nonexistent", false, false, "pretty");
assert!(result.is_err());
}
#[test]
fn test_handle_diff() {
let config = create_test_config();
let result = handle_diff(&config, "frontend", Some("backend"), false, None, "pretty");
assert!(result.is_ok());
}
}