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}