use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
use clap::{Arg, ArgMatches, Command};
use colored::*;
use std::path::Path;
use std::fs;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use toml;
#[derive(Debug, Clone)]
pub struct WorkspaceSyncTool;
#[derive(Debug, Deserialize, Serialize)]
struct WorkspaceConfig {
workspace: Workspace,
}
#[derive(Debug, Deserialize, Serialize)]
struct Workspace {
members: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
struct CargoToml {
package: Option<Package>,
dependencies: HashMap<String, Dependency>,
#[serde(rename = "dev-dependencies")]
dev_dependencies: Option<HashMap<String, Dependency>>,
}
#[derive(Debug, Deserialize, Serialize)]
struct Package {
name: String,
version: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
enum Dependency {
Simple(String),
Detailed(DependencyDetail),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct DependencyDetail {
version: Option<String>,
path: Option<String>,
git: Option<String>,
branch: Option<String>,
features: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Serialize)]
struct DependencyAnalysis {
name: String,
versions: HashMap<String, String>,
conflicts: Vec<String>,
}
impl WorkspaceSyncTool {
pub fn new() -> Self {
Self
}
fn find_workspace_root(&self) -> Result<String> {
let mut current = std::env::current_dir()
.map_err(|e| ToolError::ExecutionFailed(
format!("Cannot get current directory: {}", e),
))?;
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
let content = fs::read_to_string(&cargo_toml)
.map_err(|e| ToolError::ExecutionFailed(
format!("Cannot read Cargo.toml: {}", e),
))?;
if content.contains("[workspace]") {
return Ok(current.to_string_lossy().to_string());
}
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
Err(ToolError::ExecutionFailed("Not in a workspace".to_string()))
}
fn parse_cargo_toml(&self, path: &Path) -> Result<CargoToml> {
let content = fs::read_to_string(path)
.map_err(|e| ToolError::ExecutionFailed(
format!("Cannot read {}: {}", path.display(), e),
))?;
toml::from_str(&content)
.map_err(|e| ToolError::ExecutionFailed(
format!("Cannot parse {}: {}", path.display(), e),
))
}
fn analyze_workspace_dependencies(
&self,
workspace_root: &str,
) -> Result<HashMap<String, DependencyAnalysis>> {
let workspace_config_path = Path::new(workspace_root).join("Cargo.toml");
let workspace_config: WorkspaceConfig = {
let content = fs::read_to_string(&workspace_config_path)?;
toml::from_str(&content)?
};
let mut analyses: HashMap<String, DependencyAnalysis> = HashMap::new();
if let Ok(root_cargo) = self.parse_cargo_toml(&workspace_config_path) {
self.analyze_dependencies(
&mut analyses,
&root_cargo.dependencies,
"workspace-root",
);
if let Some(dev_deps) = &root_cargo.dev_dependencies {
self.analyze_dependencies(&mut analyses, dev_deps, "workspace-root-dev");
}
}
for member in &workspace_config.workspace.members {
let member_path = Path::new(workspace_root).join(member).join("Cargo.toml");
if let Ok(member_cargo) = self.parse_cargo_toml(&member_path) {
self.analyze_dependencies(
&mut analyses,
&member_cargo.dependencies,
&member,
);
if let Some(dev_deps) = &member_cargo.dev_dependencies {
self.analyze_dependencies(
&mut analyses,
dev_deps,
&format!("{}-dev", member),
);
}
}
}
Ok(analyses)
}
fn analyze_dependencies(
&self,
analyses: &mut HashMap<String, DependencyAnalysis>,
deps: &HashMap<String, Dependency>,
source: &str,
) {
for (name, dep) in deps {
if let Some(analysis) = analyses.get_mut(name) {
match dep {
Dependency::Simple(version) => {
analysis.versions.insert(source.to_string(), version.clone());
}
Dependency::Detailed(detail) => {
if let Some(version) = &detail.version {
analysis
.versions
.insert(source.to_string(), version.clone());
}
}
}
} else {
let mut versions = HashMap::new();
match dep {
Dependency::Simple(version) => {
versions.insert(source.to_string(), version.clone());
}
Dependency::Detailed(detail) => {
if let Some(version) = &detail.version {
versions.insert(source.to_string(), version.clone());
}
}
}
analyses
.insert(
name.clone(),
DependencyAnalysis {
name: name.clone(),
versions,
conflicts: Vec::new(),
},
);
}
}
}
fn detect_conflicts(&self, analyses: &mut HashMap<String, DependencyAnalysis>) {
for analysis in analyses.values_mut() {
if analysis.versions.len() > 1 {
let versions: Vec<&String> = analysis.versions.values().collect();
let first_version = versions[0];
for version in &versions[1..] {
if *version != first_version {
analysis
.conflicts
.push(
format!(
"Version mismatch: {} vs {}", first_version, version
),
);
}
}
}
}
}
fn sync_dependencies(&self, workspace_root: &str, dry_run: bool) -> Result<()> {
let analyses = self.analyze_workspace_dependencies(workspace_root)?;
let mut analyses = analyses;
self.detect_conflicts(&mut analyses);
if dry_run {
self.display_sync_plan(&analyses);
} else {
self.apply_sync_plan(&analyses, workspace_root)?;
}
Ok(())
}
fn display_sync_plan(&self, analyses: &HashMap<String, DependencyAnalysis>) {
println!("{}", "📋 Workspace Dependency Sync Plan".bold().blue());
println!("{}", "═".repeat(50).blue());
let conflicts: Vec<_> = analyses
.values()
.filter(|a| !a.conflicts.is_empty())
.collect();
let synced: Vec<_> = analyses
.values()
.filter(|a| a.conflicts.is_empty() && a.versions.len() > 1)
.collect();
let unique: Vec<_> = analyses
.values()
.filter(|a| a.versions.len() == 1)
.collect();
if !conflicts.is_empty() {
println!("\n{}", "🔴 Conflicts Found:".red().bold());
for analysis in &conflicts {
println!(
" {} - {} versions found", analysis.name.red(), analysis.versions
.len()
);
for conflict in &analysis.conflicts {
println!(" {}", conflict.yellow());
}
}
}
if !synced.is_empty() {
println!("\n{}", "✅ Already Synced:".green().bold());
for analysis in &synced {
if let Some(version) = analysis.versions.values().next() {
println!(
" {} - {} crates", analysis.name.green(), analysis.versions
.len()
);
}
}
}
if !unique.is_empty() {
println!("\n{}", "📦 Unique Dependencies:".cyan().bold());
for analysis in &unique {
if let Some(version) = analysis.versions.values().next() {
println!(" {} - {}", analysis.name.cyan(), version);
}
}
}
}
fn apply_sync_plan(
&self,
analyses: &HashMap<String, DependencyAnalysis>,
workspace_root: &str,
) -> Result<()> {
println!("{}", "🔄 Applying Dependency Synchronization".bold().yellow());
self.display_sync_plan(analyses);
println!("\n{}", "⚠️ Actual synchronization not yet implemented".yellow());
println!(" This would modify Cargo.toml files to ensure version consistency");
Ok(())
}
fn generate_report(
&self,
analyses: &HashMap<String, DependencyAnalysis>,
format: OutputFormat,
) -> Result<()> {
match format {
OutputFormat::Json => {
let report = serde_json::json!(
{ "workspace_analysis" : analyses, "summary" : { "total_dependencies"
: analyses.len(), "conflicts" : analyses.values().filter(| a | ! a
.conflicts.is_empty()).count(), "synced" : analyses.values().filter(|
a | a.conflicts.is_empty() && a.versions.len() > 1).count(), "unique"
: analyses.values().filter(| a | a.versions.len() == 1).count(), } }
);
println!("{}", serde_json::to_string_pretty(& report).unwrap());
}
OutputFormat::Table => {
println!(
"{:<30} {:<15} {:<10} {:<50}", "Dependency", "Versions", "Status",
"Details"
);
println!("{}", "─".repeat(105));
for analysis in analyses.values() {
let status = if !analysis.conflicts.is_empty() {
"CONFLICT".red().to_string()
} else if analysis.versions.len() > 1 {
"SYNCED".green().to_string()
} else {
"UNIQUE".cyan().to_string()
};
let details = if !analysis.conflicts.is_empty() {
analysis.conflicts.join(", ")
} else {
format!("Used in {} crates", analysis.versions.len())
};
println!(
"{:<30} {:<15} {:<10} {:<50}", analysis.name, analysis.versions
.len().to_string(), status, details.chars().take(47).collect::<
String > ()
);
}
}
OutputFormat::Human => {
self.display_sync_plan(analyses);
}
}
Ok(())
}
}
impl Tool for WorkspaceSyncTool {
fn name(&self) -> &'static str {
"workspace-sync"
}
fn description(&self) -> &'static str {
"Keep workspace dependencies in sync and manage version bumps"
}
fn command(&self) -> Command {
Command::new(self.name())
.about(self.description())
.long_about(
"Analyze and synchronize dependencies across workspace members to ensure version consistency and detect conflicts",
)
.args(
&[
Arg::new("sync-versions")
.long("sync-versions")
.help("Synchronize dependency versions across workspace")
.action(clap::ArgAction::SetTrue),
Arg::new("check-conflicts")
.long("check-conflicts")
.help("Check for version conflicts between workspace members")
.action(clap::ArgAction::SetTrue),
Arg::new("bump-minor")
.long("bump-minor")
.help("Bump minor versions for all workspace dependencies")
.action(clap::ArgAction::SetTrue),
Arg::new("bump-major")
.long("bump-major")
.help("Bump major versions for all workspace dependencies")
.action(clap::ArgAction::SetTrue),
Arg::new("bump-patch")
.long("bump-patch")
.help("Bump patch versions for all workspace dependencies")
.action(clap::ArgAction::SetTrue),
Arg::new("report")
.long("report")
.help("Generate workspace dependency report")
.action(clap::ArgAction::SetTrue),
],
)
.args(&common_options())
}
fn execute(&self, matches: &ArgMatches) -> Result<()> {
let sync_versions = matches.get_flag("sync-versions");
let check_conflicts = matches.get_flag("check-conflicts");
let bump_minor = matches.get_flag("bump-minor");
let bump_major = matches.get_flag("bump-major");
let bump_patch = matches.get_flag("bump-patch");
let report = matches.get_flag("report");
let dry_run = matches.get_flag("dry-run");
let output_format = parse_output_format(matches);
let verbose = matches.get_flag("verbose");
if verbose {
println!(
"🔍 {} - Analyzing workspace dependencies", "CargoMate WorkspaceSync"
.bold().blue()
);
}
let workspace_root = self.find_workspace_root()?;
if verbose {
println!("📁 Workspace root: {}", workspace_root.cyan());
}
let mut analyses = self.analyze_workspace_dependencies(&workspace_root)?;
self.detect_conflicts(&mut analyses);
if report {
self.generate_report(&analyses, output_format)?;
} else if check_conflicts {
let conflicts: Vec<_> = analyses
.values()
.filter(|a| !a.conflicts.is_empty())
.collect();
if conflicts.is_empty() {
println!("✅ No dependency conflicts found!");
} else {
println!("🔴 Found {} dependency conflicts:", conflicts.len());
for analysis in &conflicts {
println!(
" {}: {}", analysis.name.red(), analysis.conflicts.join(", ")
);
}
}
} else if sync_versions {
self.sync_dependencies(&workspace_root, dry_run)?;
} else if bump_minor || bump_major || bump_patch {
println!("🔄 Version bumping not yet implemented");
println!(
" This would bump versions across all workspace Cargo.toml files"
);
} else {
self.generate_report(&analyses, output_format)?;
}
Ok(())
}
}
impl Default for WorkspaceSyncTool {
fn default() -> Self {
Self::new()
}
}