Skip to content

Commit d8f58fd

Browse files
Alex HolmbergAlex793x
authored andcommitted
feat: returning dependencies as a string, for MCP server opportunity
1 parent cc1eaa1 commit d8f58fd

3 files changed

Lines changed: 125 additions & 300 deletions

File tree

src/handlers/dependencies.rs

Lines changed: 114 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ pub async fn handle_dependencies(
1313
_prod_only: bool,
1414
_dev_only: bool,
1515
format: OutputFormat,
16-
) -> crate::Result<()> {
16+
) -> crate::Result<String> {
1717
let project_path = path.canonicalize()
1818
.unwrap_or_else(|_| path.clone());
1919

20-
println!("🔍 Analyzing dependencies: {}", project_path.display());
20+
let mut output = String::new();
21+
let header = format!("🔍 Analyzing dependencies: {}\n", project_path.display());
22+
println!("{}", header);
23+
output.push_str(&header);
2124

2225
// First, analyze the project using monorepo analysis
2326
let monorepo_analysis = analyze_monorepo(&project_path)?;
@@ -36,17 +39,20 @@ pub async fn handle_dependencies(
3639
).await?;
3740

3841
if format == OutputFormat::Table {
39-
display_dependencies_table(&dep_analysis, &monorepo_analysis, licenses, vulnerabilities, &all_languages, &project_path).await?;
42+
let table_output = display_dependencies_table(&dep_analysis, &monorepo_analysis, licenses, vulnerabilities, &all_languages, &project_path).await?;
43+
output.push_str(&table_output);
4044
} else if format == OutputFormat::Json {
4145
// JSON output
42-
let output = serde_json::json!({
46+
let json_data = serde_json::json!({
4347
"dependencies": dep_analysis.dependencies,
4448
"total": dep_analysis.dependencies.len(),
4549
});
46-
println!("{}", serde_json::to_string_pretty(&output)?);
50+
let json_output = serde_json::to_string_pretty(&json_data)?;
51+
println!("{}", json_output);
52+
output.push_str(&json_output);
4753
}
4854

49-
Ok(())
55+
Ok(output)
5056
}
5157

5258
async fn display_dependencies_table(
@@ -56,58 +62,74 @@ async fn display_dependencies_table(
5662
vulnerabilities: bool,
5763
all_languages: &[analyzer::DetectedLanguage],
5864
project_path: &std::path::Path,
59-
) -> crate::Result<()> {
65+
) -> crate::Result<String> {
6066
use termcolor::{ColorChoice, StandardStream, WriteColor, ColorSpec, Color};
6167

68+
let mut output = String::new();
6269
let mut stdout = StandardStream::stdout(ColorChoice::Always);
6370

6471
// Print summary
65-
println!("\n📦 Dependency Analysis Report");
66-
println!("{}", "=".repeat(80));
72+
let summary_header = format!("\n📦 Dependency Analysis Report\n{}\n", "=".repeat(80));
73+
println!("{}", summary_header);
74+
output.push_str(&summary_header);
6775

68-
let total_deps: usize = dep_analysis.dependencies.len();
69-
println!("Total dependencies: {}", total_deps);
76+
let total_deps_line = format!("Total dependencies: {}\n", dep_analysis.dependencies.len());
77+
println!("{}", total_deps_line);
78+
output.push_str(&total_deps_line);
7079

7180
if monorepo_analysis.is_monorepo {
72-
println!("Projects analyzed: {}", monorepo_analysis.projects.len());
81+
let projects_line = format!("Projects analyzed: {}\n", monorepo_analysis.projects.len());
82+
println!("{}", projects_line);
83+
output.push_str(&projects_line);
7384
for project in &monorepo_analysis.projects {
74-
println!(" • {} ({})", project.name, format_project_category(&project.project_category));
85+
let project_line = format!(" • {} ({})\n", project.name, format_project_category(&project.project_category));
86+
println!("{}", project_line);
87+
output.push_str(&project_line);
7588
}
7689
}
7790

7891
for (name, info) in &dep_analysis.dependencies {
79-
print!(" {} v{}", name, info.version);
92+
let dep_line = format!(" {} v{}", name, info.version);
93+
print!("{}", dep_line);
94+
output.push_str(&dep_line);
8095

8196
// Color code by type
8297
stdout.set_color(ColorSpec::new().set_fg(Some(
8398
if info.is_dev { Color::Yellow } else { Color::Green }
8499
)))?;
85100

86-
print!(" [{}]", if info.is_dev { "dev" } else { "prod" });
101+
let type_tag = format!(" [{}]", if info.is_dev { "dev" } else { "prod" });
102+
print!("{}", type_tag);
103+
output.push_str(&type_tag);
87104

88105
stdout.reset()?;
89106

90107
if licenses && info.license.is_some() {
91-
print!(" - License: {}", info.license.as_ref().unwrap_or(&"Unknown".to_string()));
108+
let license_info = format!(" - License: {}", info.license.as_ref().unwrap_or(&"Unknown".to_string()));
109+
print!("{}", license_info);
110+
output.push_str(&license_info);
92111
}
93112

94113
println!();
114+
output.push('\n');
95115
}
96116

97117
if licenses {
98-
display_license_summary(&dep_analysis.dependencies);
118+
let license_output = display_license_summary(&dep_analysis.dependencies);
119+
output.push_str(&license_output);
99120
}
100121

101122
if vulnerabilities {
102-
check_and_display_vulnerabilities(dep_analysis, all_languages, project_path).await?;
123+
let vuln_output = check_and_display_vulnerabilities(dep_analysis, all_languages, project_path).await?;
124+
output.push_str(&vuln_output);
103125
}
104126

105-
Ok(())
127+
Ok(output)
106128
}
107129

108-
fn display_license_summary(dependencies: &analyzer::dependency_parser::DetailedDependencyMap) {
109-
println!("\n📋 License Summary");
110-
println!("{}", "-".repeat(80));
130+
fn display_license_summary(dependencies: &analyzer::dependency_parser::DetailedDependencyMap) -> String {
131+
let mut output = String::new();
132+
output.push_str(&format!("\n📋 License Summary\n{}\n", "-".repeat(80)));
111133

112134
let mut license_counts: HashMap<String, usize> = HashMap::new();
113135

@@ -121,18 +143,24 @@ fn display_license_summary(dependencies: &analyzer::dependency_parser::DetailedD
121143
licenses.sort_by(|a, b| b.1.cmp(&a.1));
122144

123145
for (license, count) in licenses {
124-
println!(" {}: {} packages", license, count);
146+
output.push_str(&format!(" {}: {} packages\n", license, count));
125147
}
148+
149+
println!("{}", output);
150+
output
126151
}
127152

128153
async fn check_and_display_vulnerabilities(
129154
dep_analysis: &analyzer::dependency_parser::DependencyAnalysis,
130155
all_languages: &[analyzer::DetectedLanguage],
131156
project_path: &std::path::Path,
132-
) -> crate::Result<()> {
157+
) -> crate::Result<String> {
133158
use termcolor::{ColorChoice, StandardStream, WriteColor, ColorSpec, Color};
134159

160+
let mut output = String::new();
161+
135162
println!("\n🔍 Checking for vulnerabilities...");
163+
output.push_str("\n🔍 Checking for vulnerabilities...\n");
136164

137165
// Convert DetailedDependencyMap to the format expected by VulnerabilityChecker
138166
let mut deps_by_language: HashMap<analyzer::dependency_parser::Language, Vec<analyzer::dependency_parser::DependencyInfo>> = HashMap::new();
@@ -199,17 +227,25 @@ async fn check_and_display_vulnerabilities(
199227
Ok(report) => {
200228
let mut stdout = StandardStream::stdout(ColorChoice::Always);
201229

202-
println!("\n🛡️ Vulnerability Report");
203-
println!("{}", "-".repeat(80));
204-
println!("Checked at: {}", report.checked_at.format("%Y-%m-%d %H:%M:%S UTC"));
205-
println!("Total vulnerabilities: {}", report.total_vulnerabilities);
230+
let report_header = format!("\n🛡️ Vulnerability Report\n{}\nChecked at: {}\nTotal vulnerabilities: {}\n",
231+
"-".repeat(80),
232+
report.checked_at.format("%Y-%m-%d %H:%M:%S UTC"),
233+
report.total_vulnerabilities
234+
);
235+
println!("{}", report_header);
236+
output.push_str(&report_header);
206237

207238
if report.total_vulnerabilities > 0 {
208-
display_vulnerability_breakdown(&report, &mut stdout)?;
209-
display_vulnerable_dependencies(&report, &mut stdout)?;
239+
let breakdown_output = display_vulnerability_breakdown(&report, &mut stdout)?;
240+
output.push_str(&breakdown_output);
241+
242+
let deps_output = display_vulnerable_dependencies(&report, &mut stdout)?;
243+
output.push_str(&deps_output);
210244
} else {
211245
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
212-
println!("\n✅ No known vulnerabilities found!");
246+
let no_vulns_message = "\n✅ No known vulnerabilities found!\n";
247+
println!("{}", no_vulns_message);
248+
output.push_str(no_vulns_message);
213249
stdout.reset()?;
214250
}
215251
}
@@ -219,56 +255,72 @@ async fn check_and_display_vulnerabilities(
219255
}
220256
}
221257

222-
Ok(())
258+
Ok(output)
223259
}
224260

225261
fn display_vulnerability_breakdown(
226262
report: &analyzer::vulnerability_checker::VulnerabilityReport,
227263
stdout: &mut termcolor::StandardStream,
228-
) -> crate::Result<()> {
264+
) -> crate::Result<String> {
229265
use termcolor::{WriteColor, ColorSpec, Color};
230266

231-
println!("\nSeverity Breakdown:");
267+
let mut output = String::new();
268+
269+
output.push_str("\nSeverity Breakdown:\n");
232270
if report.critical_count > 0 {
233271
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?;
234-
println!(" CRITICAL: {}", report.critical_count);
272+
let critical_line = format!(" CRITICAL: {}\n", report.critical_count);
273+
output.push_str(&critical_line);
274+
print!("{}", critical_line);
235275
stdout.reset()?;
236276
}
237277
if report.high_count > 0 {
238278
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
239-
println!(" HIGH: {}", report.high_count);
279+
let high_line = format!(" HIGH: {}\n", report.high_count);
280+
output.push_str(&high_line);
281+
print!("{}", high_line);
240282
stdout.reset()?;
241283
}
242284
if report.medium_count > 0 {
243285
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
244-
println!(" MEDIUM: {}", report.medium_count);
286+
let medium_line = format!(" MEDIUM: {}\n", report.medium_count);
287+
output.push_str(&medium_line);
288+
print!("{}", medium_line);
245289
stdout.reset()?;
246290
}
247291
if report.low_count > 0 {
248292
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
249-
println!(" LOW: {}", report.low_count);
293+
let low_line = format!(" LOW: {}\n", report.low_count);
294+
output.push_str(&low_line);
295+
print!("{}", low_line);
250296
stdout.reset()?;
251297
}
252298

253-
Ok(())
299+
Ok(output)
254300
}
255301

256302
fn display_vulnerable_dependencies(
257303
report: &analyzer::vulnerability_checker::VulnerabilityReport,
258304
stdout: &mut termcolor::StandardStream,
259-
) -> crate::Result<()> {
305+
) -> crate::Result<String> {
260306
use termcolor::{WriteColor, ColorSpec, Color};
261307

262-
println!("\nVulnerable Dependencies:");
308+
let mut output = String::new();
309+
310+
output.push_str("\nVulnerable Dependencies:\n");
263311
for vuln_dep in &report.vulnerable_dependencies {
264-
println!("\n 📦 {} v{} ({})",
312+
let dep_line = format!("\n 📦 {} v{} ({})\n",
265313
vuln_dep.name,
266314
vuln_dep.version,
267315
vuln_dep.language.as_str()
268316
);
317+
output.push_str(&dep_line);
318+
print!("{}", dep_line);
269319

270320
for vuln in &vuln_dep.vulnerabilities {
271-
print!(" ⚠️ {} ", vuln.id);
321+
let vuln_id_line = format!(" ⚠️ {} ", vuln.id);
322+
output.push_str(&vuln_id_line);
323+
print!("{}", vuln_id_line);
272324

273325
// Color by severity
274326
stdout.set_color(ColorSpec::new().set_fg(Some(
@@ -281,28 +333,36 @@ fn display_vulnerable_dependencies(
281333
}
282334
)).set_bold(vuln.severity == VulnerabilitySeverity::Critical))?;
283335

284-
print!("[{}]", match vuln.severity {
285-
VulnerabilitySeverity::Critical => "CRITICAL",
286-
VulnerabilitySeverity::High => "HIGH",
287-
VulnerabilitySeverity::Medium => "MEDIUM",
288-
VulnerabilitySeverity::Low => "LOW",
289-
VulnerabilitySeverity::Info => "INFO",
290-
});
336+
let severity_tag = match vuln.severity {
337+
VulnerabilitySeverity::Critical => "[CRITICAL]",
338+
VulnerabilitySeverity::High => "[HIGH]",
339+
VulnerabilitySeverity::Medium => "[MEDIUM]",
340+
VulnerabilitySeverity::Low => "[LOW]",
341+
VulnerabilitySeverity::Info => "[INFO]",
342+
};
343+
output.push_str(severity_tag);
344+
print!("{}", severity_tag);
291345

292346
stdout.reset()?;
293347

294-
println!(" - {}", vuln.title);
348+
let title_line = format!(" - {}\n", vuln.title);
349+
output.push_str(&title_line);
350+
print!("{}", title_line);
295351

296352
if let Some(ref cve) = vuln.cve {
297-
println!(" CVE: {}", cve);
353+
let cve_line = format!(" CVE: {}\n", cve);
354+
output.push_str(&cve_line);
355+
println!("{}", cve_line.trim_end());
298356
}
299357
if let Some(ref patched) = vuln.patched_versions {
300358
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
301-
println!(" Fix: Upgrade to {}", patched);
359+
let fix_line = format!(" Fix: Upgrade to {}\n", patched);
360+
output.push_str(&fix_line);
361+
println!("{}", fix_line.trim_end());
302362
stdout.reset()?;
303363
}
304364
}
305365
}
306366

307-
Ok(())
367+
Ok(output)
308368
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub async fn run_command(command: Commands) -> Result<()> {
7272
handlers::handle_support(languages, frameworks, detailed)
7373
}
7474
Commands::Dependencies { path, licenses, vulnerabilities, prod_only, dev_only, format } => {
75-
handlers::handle_dependencies(path, licenses, vulnerabilities, prod_only, dev_only, format).await
75+
handlers::handle_dependencies(path, licenses, vulnerabilities, prod_only, dev_only, format).await.map(|_| ())
7676
}
7777
Commands::Vulnerabilities { path, severity, format, output } => {
7878
handlers::handle_vulnerabilities(path, severity, format, output).await

0 commit comments

Comments
 (0)