Skip to main content

cargo_mate/captain/
treasure_map.rs

1use anyhow::{Context, Result};
2use cargo_metadata::{DependencyKind, Metadata, MetadataCommand, Package, PackageId};
3use colored::*;
4use petgraph::graph::{DiGraph, NodeIndex};
5use std::collections::{HashMap, HashSet};
6use std::fs;
7use std::path::PathBuf;
8use std::process::Command;
9use crate::license;
10#[derive(Debug, Clone)]
11pub struct DependencyNode {
12    pub name: String,
13    pub version: String,
14    pub source: Option<String>,
15    pub features: Vec<String>,
16    pub size_bytes: Option<u64>,
17    pub license: Option<String>,
18    pub is_dev: bool,
19    pub is_build: bool,
20    pub depth: usize,
21}
22pub struct TreasureMap {
23    graph: DiGraph<DependencyNode, DependencyKind>,
24    node_map: HashMap<PackageId, NodeIndex>,
25    metadata: Metadata,
26    root_package: Option<Package>,
27}
28impl TreasureMap {
29    pub fn new() -> Result<Self> {
30        let metadata = MetadataCommand::new()
31            .exec()
32            .context("Failed to get cargo metadata")?;
33        let root_package = metadata.root_package().cloned();
34        let mut map = Self {
35            graph: DiGraph::new(),
36            node_map: HashMap::new(),
37            metadata: metadata.clone(),
38            root_package,
39        };
40        map.build_graph()?;
41        Ok(map)
42    }
43    fn build_graph(&mut self) -> Result<()> {
44        for package in &self.metadata.packages {
45            let node = DependencyNode {
46                name: package.name.clone(),
47                version: package.version.to_string(),
48                source: package.source.as_ref().map(|s| s.to_string()),
49                features: package.features.keys().cloned().collect(),
50                size_bytes: self.estimate_package_size(package),
51                license: package.license.clone(),
52                is_dev: false,
53                is_build: false,
54                depth: 0,
55            };
56            let idx = self.graph.add_node(node);
57            self.node_map.insert(package.id.clone(), idx);
58        }
59        for package in &self.metadata.packages {
60            let from_idx = *self.node_map.get(&package.id).unwrap();
61            for dep in &package.dependencies {
62                if let Some(target_package) = self.find_package(&dep.name, &dep.req) {
63                    let to_idx = *self.node_map.get(&target_package.id).unwrap();
64                    self.graph.add_edge(from_idx, to_idx, dep.kind);
65                    if dep.kind == DependencyKind::Development {
66                        self.graph[to_idx].is_dev = true;
67                    }
68                    if dep.kind == DependencyKind::Build {
69                        self.graph[to_idx].is_build = true;
70                    }
71                }
72            }
73        }
74        self.calculate_depths();
75        Ok(())
76    }
77    fn find_package(&self, name: &str, _req: &semver::VersionReq) -> Option<&Package> {
78        self.metadata.packages.iter().find(|p| p.name == name)
79    }
80    fn estimate_package_size(&self, package: &Package) -> Option<u64> {
81        let mut size = 0u64;
82        for target in &package.targets {
83            if let Ok(metadata) = fs::metadata(&target.src_path) {
84                size += metadata.len();
85            }
86        }
87        if size > 0 { Some(size) } else { None }
88    }
89    fn calculate_depths(&mut self) {
90        if let Some(ref root) = self.root_package {
91            if let Some(&root_idx) = self.node_map.get(&root.id) {
92                let mut visited = HashSet::new();
93                self.calculate_depth_recursive(root_idx, 0, &mut visited);
94            }
95        }
96    }
97    fn calculate_depth_recursive(
98        &mut self,
99        node: NodeIndex,
100        depth: usize,
101        visited: &mut HashSet<NodeIndex>,
102    ) {
103        if visited.contains(&node) {
104            return;
105        }
106        visited.insert(node);
107        self.graph[node].depth = depth;
108        let neighbors: Vec<NodeIndex> = self.graph.neighbors(node).collect();
109        for neighbor in neighbors {
110            self.calculate_depth_recursive(neighbor, depth + 1, visited);
111        }
112    }
113    pub fn show_map(&self) {
114        println!("{}", "πŸ—ΊοΈ  Treasure Map - Dependency Visualization".blue().bold());
115        println!();
116        if let Some(ref root) = self.root_package {
117            if let Some(&root_idx) = self.node_map.get(&root.id) {
118                self.print_tree(root_idx, "", true, &mut HashSet::new());
119            }
120        }
121    }
122    fn print_tree(
123        &self,
124        node: NodeIndex,
125        prefix: &str,
126        is_last: bool,
127        visited: &mut HashSet<NodeIndex>,
128    ) {
129        let dep = &self.graph[node];
130        let icon = self.get_node_icon(dep);
131        let color = self.get_node_color(dep);
132        let node_str = format!("{} {} v{}", icon, dep.name, dep.version);
133        let colored_str = match color {
134            NodeColor::Green => node_str.green(),
135            NodeColor::Yellow => node_str.yellow(),
136            NodeColor::Red => node_str.red(),
137            NodeColor::Blue => node_str.blue(),
138            NodeColor::Gray => node_str.dimmed(),
139        };
140        let display_str = if visited.contains(&node) {
141            format!("{} [circular]", colored_str)
142        } else {
143            colored_str.to_string()
144        };
145        println!(
146            "{}{}{}", prefix, if is_last { "└── " } else { "β”œβ”€β”€ " },
147            display_str
148        );
149        if visited.contains(&node) {
150            return;
151        }
152        visited.insert(node);
153        let mut children: Vec<NodeIndex> = self.graph.neighbors(node).collect();
154        children.sort_by_key(|&idx| &self.graph[idx].name);
155        for (i, child) in children.iter().enumerate() {
156            let is_last_child = i == children.len() - 1;
157            let new_prefix = format!(
158                "{}{}", prefix, if is_last { "    " } else { "β”‚   " }
159            );
160            self.print_tree(*child, &new_prefix, is_last_child, visited);
161        }
162    }
163    fn get_node_icon(&self, node: &DependencyNode) -> &str {
164        if node.source.is_none() {
165            "πŸ“¦"
166        } else if node.is_dev {
167            "πŸ”§"
168        } else if node.is_build {
169            "πŸ”¨"
170        } else {
171            "πŸ“š"
172        }
173    }
174    fn get_node_color(&self, node: &DependencyNode) -> NodeColor {
175        if node.source.is_none() {
176            NodeColor::Green
177        } else if node.is_dev {
178            NodeColor::Blue
179        } else if node.depth > 3 {
180            NodeColor::Gray
181        } else {
182            NodeColor::Yellow
183        }
184    }
185    pub fn analyze(&self) -> DependencyAnalysis {
186        let total_dependencies = self.graph.node_count();
187        let direct_dependencies = self.count_direct_dependencies();
188        let dev_dependencies = self.graph.node_weights().filter(|n| n.is_dev).count();
189        let max_depth = self.graph.node_weights().map(|n| n.depth).max().unwrap_or(0);
190        let duplicate_deps = self.find_duplicate_dependencies();
191        let circular_deps = self.find_circular_dependencies();
192        let outdated_deps = self.check_outdated_dependencies();
193        let security_issues = self.check_security_issues();
194        let total_size = self.graph.node_weights().filter_map(|n| n.size_bytes).sum();
195        let largest_deps = self.find_largest_dependencies(10);
196        DependencyAnalysis {
197            total_dependencies,
198            direct_dependencies,
199            dev_dependencies,
200            max_depth,
201            duplicate_deps,
202            circular_deps,
203            outdated_deps,
204            security_issues,
205            total_size,
206            largest_deps,
207        }
208    }
209    fn count_direct_dependencies(&self) -> usize {
210        if let Some(ref root) = self.root_package {
211            if let Some(&root_idx) = self.node_map.get(&root.id) {
212                return self.graph.neighbors(root_idx).count();
213            }
214        }
215        0
216    }
217    fn find_duplicate_dependencies(&self) -> Vec<DuplicateDependency> {
218        let mut deps_by_name: HashMap<String, Vec<(String, NodeIndex)>> = HashMap::new();
219        for idx in self.graph.node_indices() {
220            let node = &self.graph[idx];
221            deps_by_name
222                .entry(node.name.clone())
223                .or_default()
224                .push((node.version.clone(), idx));
225        }
226        let mut duplicates = Vec::new();
227        for (name, versions) in deps_by_name {
228            if versions.len() > 1 {
229                duplicates
230                    .push(DuplicateDependency {
231                        name,
232                        versions: versions.into_iter().map(|(v, _)| v).collect(),
233                    });
234            }
235        }
236        duplicates
237    }
238    fn find_circular_dependencies(&self) -> Vec<Vec<String>> {
239        let mut cycles = Vec::new();
240        let mut visited = HashSet::new();
241        let mut stack = Vec::new();
242        for idx in self.graph.node_indices() {
243            if !visited.contains(&idx) {
244                self.dfs_cycles(idx, &mut visited, &mut stack, &mut cycles);
245            }
246        }
247        cycles
248    }
249    fn dfs_cycles(
250        &self,
251        node: NodeIndex,
252        visited: &mut HashSet<NodeIndex>,
253        stack: &mut Vec<NodeIndex>,
254        cycles: &mut Vec<Vec<String>>,
255    ) {
256        visited.insert(node);
257        stack.push(node);
258        for neighbor in self.graph.neighbors(node) {
259            if let Some(pos) = stack.iter().position(|&n| n == neighbor) {
260                let cycle: Vec<String> = stack[pos..]
261                    .iter()
262                    .map(|&idx| self.graph[idx].name.clone())
263                    .collect();
264                cycles.push(cycle);
265            } else if !visited.contains(&neighbor) {
266                self.dfs_cycles(neighbor, visited, stack, cycles);
267            }
268        }
269        stack.pop();
270    }
271    fn check_outdated_dependencies(&self) -> Vec<OutdatedDependency> {
272        let output = Command::new("cargo")
273            .args(&["outdated", "--format", "json"])
274            .output();
275        match output {
276            Ok(output) if output.status.success() => Vec::new(),
277            _ => Vec::new(),
278        }
279    }
280    fn check_security_issues(&self) -> Vec<SecurityIssue> {
281        let output = Command::new("cargo").args(&["audit", "--json"]).output();
282        match output {
283            Ok(output) if output.status.success() => Vec::new(),
284            _ => Vec::new(),
285        }
286    }
287    fn find_largest_dependencies(&self, limit: usize) -> Vec<(String, u64)> {
288        let mut deps_with_size: Vec<(String, u64)> = self
289            .graph
290            .node_weights()
291            .filter_map(|n| {
292                n.size_bytes.map(|s| (format!("{} v{}", n.name, n.version), s))
293            })
294            .collect();
295        deps_with_size.sort_by(|a, b| b.1.cmp(&a.1));
296        deps_with_size.truncate(limit);
297        deps_with_size
298    }
299    pub fn export_dot(&self, path: &PathBuf) -> Result<()> {
300        let mut dot = String::new();
301        dot.push_str("digraph dependencies {\n");
302        dot.push_str("    rankdir=LR;\n");
303        dot.push_str("    node [shape=box];\n\n");
304        for idx in self.graph.node_indices() {
305            let node = &self.graph[idx];
306            let color = match self.get_node_color(node) {
307                NodeColor::Green => "green",
308                NodeColor::Yellow => "yellow",
309                NodeColor::Red => "red",
310                NodeColor::Blue => "blue",
311                NodeColor::Gray => "gray",
312            };
313            dot.push_str(
314                &format!(
315                    "    \"{}\" [label=\"{}\\nv{}\", color=\"{}\"];\n", node.name, node
316                    .name, node.version, color
317                ),
318            );
319        }
320        for edge in self.graph.edge_indices() {
321            let (from, to) = self.graph.edge_endpoints(edge).unwrap();
322            let from_name = &self.graph[from].name;
323            let to_name = &self.graph[to].name;
324            let style = match self.graph[edge] {
325                DependencyKind::Development => "dashed",
326                DependencyKind::Build => "dotted",
327                _ => "solid",
328            };
329            dot.push_str(
330                &format!(
331                    "    \"{}\" -> \"{}\" [style=\"{}\"];\n", from_name, to_name, style
332                ),
333            );
334        }
335        dot.push_str("}\n");
336        fs::write(path, dot)?;
337        println!("βœ… Dependency graph exported to {}", path.display());
338        Ok(())
339    }
340    pub fn find_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
341        let from_idx = self
342            .graph
343            .node_indices()
344            .find(|&idx| self.graph[idx].name == from)?;
345        let to_idx = self.graph.node_indices().find(|&idx| self.graph[idx].name == to)?;
346        let path = petgraph::algo::astar(
347            &self.graph,
348            from_idx,
349            |n| n == to_idx,
350            |_| 1,
351            |_| 0,
352        );
353        path.map(|(_, p)| {
354            p.into_iter().map(|idx| self.graph[idx].name.clone()).collect()
355        })
356    }
357    pub fn find_unused(&self) -> Vec<String> {
358        let output = Command::new("cargo").args(&["machete"]).output();
359        match output {
360            Ok(output) if output.status.success() => {
361                String::from_utf8_lossy(&output.stdout)
362                    .lines()
363                    .filter(|l| !l.is_empty())
364                    .map(|l| l.to_string())
365                    .collect()
366            }
367            _ => Vec::new(),
368        }
369    }
370}
371#[derive(Debug)]
372pub struct DependencyAnalysis {
373    pub total_dependencies: usize,
374    pub direct_dependencies: usize,
375    pub dev_dependencies: usize,
376    pub max_depth: usize,
377    pub duplicate_deps: Vec<DuplicateDependency>,
378    pub circular_deps: Vec<Vec<String>>,
379    pub outdated_deps: Vec<OutdatedDependency>,
380    pub security_issues: Vec<SecurityIssue>,
381    pub total_size: u64,
382    pub largest_deps: Vec<(String, u64)>,
383}
384impl DependencyAnalysis {
385    pub fn display(&self) {
386        println!("{}", "=== Dependency Analysis ===".blue().bold());
387        println!("πŸ“Š Total dependencies: {}", self.total_dependencies);
388        println!("   Direct: {}", self.direct_dependencies);
389        println!("   Dev: {}", self.dev_dependencies);
390        println!("   Max depth: {}", self.max_depth);
391        if self.total_size > 0 {
392            println!("πŸ’Ύ Total size: {}", format_size(self.total_size));
393        }
394        if !self.duplicate_deps.is_empty() {
395            println!(
396                "\n⚠️  {} duplicate dependencies found:", self.duplicate_deps.len()
397                .to_string().yellow()
398            );
399            for dup in &self.duplicate_deps[..5.min(self.duplicate_deps.len())] {
400                println!(
401                    "   {} has versions: {}", dup.name.yellow(), dup.versions.join(", ")
402                );
403            }
404        }
405        if !self.circular_deps.is_empty() {
406            println!(
407                "\nπŸ”„ {} circular dependencies found:", self.circular_deps.len()
408                .to_string().red()
409            );
410            for cycle in &self.circular_deps[..3.min(self.circular_deps.len())] {
411                println!("   {}", cycle.join(" β†’ ").red());
412            }
413        }
414        if !self.largest_deps.is_empty() {
415            println!("\nπŸ“¦ Largest dependencies:");
416            for (name, size) in &self.largest_deps[..5.min(self.largest_deps.len())] {
417                println!("   {} - {}", name, format_size(* size));
418            }
419        }
420        if !self.security_issues.is_empty() {
421            println!(
422                "\n🚨 {} security issues found!", self.security_issues.len()
423                .to_string().red().bold()
424            );
425            for issue in &self.security_issues[..3.min(self.security_issues.len())] {
426                println!("   {} - {}", issue.package.red(), issue.advisory);
427            }
428        }
429    }
430}
431#[derive(Debug)]
432pub struct DuplicateDependency {
433    pub name: String,
434    pub versions: Vec<String>,
435}
436#[derive(Debug)]
437pub struct OutdatedDependency {
438    pub name: String,
439    pub current_version: String,
440    pub latest_version: String,
441}
442#[derive(Debug)]
443pub struct SecurityIssue {
444    pub package: String,
445    pub advisory: String,
446    pub severity: String,
447}
448#[derive(Debug)]
449enum NodeColor {
450    Green,
451    Yellow,
452    Red,
453    Blue,
454    Gray,
455}
456fn format_size(bytes: u64) -> String {
457    const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
458    let mut size = bytes as f64;
459    let mut unit_idx = 0;
460    while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
461        size /= 1024.0;
462        unit_idx += 1;
463    }
464    format!("{:.2} {}", size, UNITS[unit_idx])
465}
466pub fn check_bosun_quotas(command: &str) -> Result<bool> {
467    println!("πŸ”¨ Bosun checking quotas for command '{}' - tally ho!", command.cyan());
468    let license_manager = license::LicenseManager::new()?;
469    match license_manager.enforce_license(command) {
470        Ok(_) => {
471            println!(
472                "βœ… Bosun reports: Command '{}' within quota limits!", command.green()
473            );
474            println!("   πŸ”¨ All tallies accounted for - ready to proceed!");
475            Ok(true)
476        }
477        Err(e) => {
478            if e.to_string().contains("limit") {
479                println!("⚠️  Bosun warning: Command quota exceeded!");
480                println!("   πŸ”¨ Requisition more at: https://cargo.do/checkout");
481                println!("   πŸ”¨ Upgrade for unlimited command tallies");
482            } else if e.to_string().contains("License not found") {
483                println!("❌ Bosun emergency: No quota authorization!");
484                println!("   πŸ”¨ Get requisition with 'cm register <key>'");
485            } else {
486                println!(
487                    "❌ Bosun distress: Quota check failed: {}", e.to_string().red()
488                );
489                println!("   πŸ”¨ Secure the manifest - prepare for inspection!");
490            }
491            Ok(false)
492        }
493    }
494}