use anyhow::{Context, Result};
use depbank::{
DependencyCollection, calculate_directory_tokens, calculate_file_tokens, collect_dependencies,
extract_dependency_info, find_cargo_lock, find_cargo_toml_files, generate_all_code_banks,
is_dependency_available, resolve_dependency_versions, resolve_registry_path,
};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
const README_HEADER: &str = "# Code Bank Summary\n\n";
const README_TABLE_HEADER: &str = "| Dependency | Version | Tokens | Size (bytes) |\n";
const README_TABLE_SEPARATOR: &str = "|------------|---------|--------|-------------|\n";
const README_TOKEN_SUMMARY_HEADER: &str = "\n## Token Summary\n\n";
const README_ABOUT_HEADER: &str = "\n## About Code Banks\n\n";
const README_ABOUT_P1: &str = "Code banks are generated summaries of your project's dependencies. ";
const README_ABOUT_P2: &str = "They provide an overview of the code structure and key components ";
const README_ABOUT_P3: &str =
"of each dependency, helping you understand the libraries your project uses ";
const README_ABOUT_P4: &str = "without having to browse through the original source code.\n\n";
const README_ABOUT_P5: &str =
"Each file contains a summary of the corresponding dependency's source code, ";
const README_ABOUT_P6: &str = "including important types, functions, and structures.\n\n";
const README_ABOUT_P7: &str = "Generated by [DepBank](https://github.com/tyrchen/depbank).\n";
pub fn generate_command(project_path: &Path, output_dir: &Path, dry_run: bool) -> Result<()> {
println!("Analyzing project...");
let available_deps = analyze_dependencies(project_path, dry_run)?;
if dry_run {
println!("Dry run enabled, skipping generation");
return Ok(());
}
println!("Generating code banks...");
let registry_path = resolve_registry_path()?;
let code_bank_files = generate_all_code_banks(&available_deps, ®istry_path, output_dir)?;
println!("Generated {} code bank files", code_bank_files.len());
generate_code_bank_readme(
output_dir,
project_path,
&available_deps,
code_bank_files.len(),
)?;
Ok(())
}
fn analyze_dependencies(project_path: &Path, _dry_run: bool) -> Result<DependencyCollection> {
let cargo_toml_files = find_cargo_toml_files(project_path)?;
println!("Found {} Cargo.toml files", cargo_toml_files.len());
if cargo_toml_files.is_empty() {
return Err(anyhow::anyhow!("No Cargo.toml files found"));
}
let first_cargo_toml = &cargo_toml_files[0];
let dependency_info = extract_dependency_info(first_cargo_toml)?;
println!("Found {} dependencies", dependency_info.len());
let cargo_lock_path = find_cargo_lock(project_path)?;
println!("Found Cargo.lock");
let resolved_versions = resolve_dependency_versions(cargo_lock_path, &dependency_info)?;
println!("Resolved {} versions", resolved_versions.len());
let registry_path = resolve_registry_path()?;
let mut available_deps = DependencyCollection::new();
for dependency in resolved_versions.iter() {
if is_dependency_available(®istry_path, dependency) {
available_deps.add(dependency.clone());
}
}
println!(
"{}/{} dependencies available locally",
available_deps.len(),
resolved_versions.len()
);
if available_deps.is_empty() {
return Err(anyhow::anyhow!("No dependencies available locally"));
}
Ok(available_deps)
}
fn generate_code_bank_readme(
output_dir: &Path,
project_path: &Path,
dependencies: &DependencyCollection,
code_bank_files_count: usize,
) -> Result<()> {
println!("Calculating tokens for generated code banks (may take a while)...");
let file_stats = calculate_directory_tokens(output_dir, Some("md"))?;
let mut stats_vec: Vec<_> = file_stats.iter().collect();
stats_vec.sort_by(|a, b| b.1.token_count.cmp(&a.1.token_count));
let (readme_content, total_tokens) = create_readme_content(
project_path,
code_bank_files_count,
&stats_vec,
dependencies,
&file_stats,
);
let readme_path = output_dir.join("README.md");
fs::write(&readme_path, readme_content).with_context(|| {
format!(
"Failed to write README.md to file: {}",
readme_path.display()
)
})?;
println!("\nSummary:");
println!("- Generated {} code bank files", code_bank_files_count);
println!("- Total tokens: {}", total_tokens);
println!("- Added README.md with summary and token information");
println!("- Output directory: {}", output_dir.display());
Ok(())
}
fn create_readme_content(
project_path: &Path,
code_bank_files_count: usize,
stats_vec: &[(&String, &depbank::FileStats)],
dependencies: &DependencyCollection,
file_stats: &HashMap<String, depbank::FileStats>,
) -> (String, usize) {
let mut total_tokens = 0;
let mut total_size = 0;
let mut readme_content = String::new();
readme_content.push_str(README_HEADER);
write!(
readme_content,
"Generated for project: {}\n\n",
project_path.display()
)
.unwrap();
write!(
readme_content,
"## Dependencies ({} total)\n\n",
code_bank_files_count
)
.unwrap();
readme_content.push_str(README_TABLE_HEADER);
readme_content.push_str(README_TABLE_SEPARATOR);
for (name, stats) in stats_vec {
let name_without_md = name.trim_end_matches(".md");
let version = dependencies
.get_version(name_without_md)
.map(|v| v.as_str())
.unwrap_or("unknown");
writeln!(
readme_content,
"| {} | {} | {} | {} |",
name_without_md, version, stats.token_count, stats.size_bytes
)
.unwrap();
total_tokens += stats.token_count;
total_size += stats.size_bytes;
}
readme_content.push_str(README_TOKEN_SUMMARY_HEADER);
writeln!(readme_content, "- **Total tokens:** {}", total_tokens).unwrap();
writeln!(readme_content, "- **Total size:** {} bytes", total_size).unwrap();
writeln!(
readme_content,
"- **Number of files:** {}",
file_stats.len()
)
.unwrap();
readme_content.push_str(README_ABOUT_HEADER);
readme_content.push_str(README_ABOUT_P1);
readme_content.push_str(README_ABOUT_P2);
readme_content.push_str(README_ABOUT_P3);
readme_content.push_str(README_ABOUT_P4);
readme_content.push_str(README_ABOUT_P5);
readme_content.push_str(README_ABOUT_P6);
readme_content.push_str(README_ABOUT_P7);
(readme_content, total_tokens)
}
pub fn tokens_command(path: &Path, extension: Option<&str>) -> Result<()> {
if path.is_file() {
analyze_file_tokens(path)?;
} else if path.is_dir() {
analyze_directory_tokens(path, extension)?;
} else {
return Err(anyhow::anyhow!(
"Path does not exist or is not accessible: {}",
path.display()
));
}
Ok(())
}
fn analyze_file_tokens(path: &Path) -> Result<()> {
let token_count = calculate_file_tokens(path)?;
let file_size = std::fs::metadata(path)?.len();
println!(
"{}: {} tokens, {} bytes",
path.display(),
token_count,
file_size
);
Ok(())
}
fn analyze_directory_tokens(dir_path: &Path, extension: Option<&str>) -> Result<()> {
let file_stats = calculate_directory_tokens(dir_path, extension)?;
let mut stats_vec: Vec<_> = file_stats.iter().collect();
stats_vec.sort_by(|a, b| b.1.token_count.cmp(&a.1.token_count));
let (total_tokens, total_size) = print_token_stats(dir_path, &stats_vec, file_stats.len());
println!(
"\nTotal: {} tokens, {} bytes across {} files",
total_tokens,
total_size,
file_stats.len()
);
Ok(())
}
fn print_token_stats(
dir_path: &Path,
stats_vec: &[(&String, &depbank::FileStats)],
_file_count: usize,
) -> (usize, usize) {
let mut total_tokens = 0;
let mut total_size = 0;
println!("Token counts for files in {}:", dir_path.display());
for (name, stats) in stats_vec {
println!(
"{}: {} tokens, {} bytes",
name, stats.token_count, stats.size_bytes
);
total_tokens += stats.token_count;
total_size += stats.size_bytes;
}
(total_tokens, total_size)
}
pub fn list_command(project_path: &Path, detailed: bool) -> Result<()> {
let cargo_toml_files = find_cargo_toml_files(project_path)?;
println!("Found {} Cargo.toml files", cargo_toml_files.len());
if cargo_toml_files.is_empty() {
return Err(anyhow::anyhow!("No Cargo.toml files found"));
}
let dependencies = collect_dependencies(&cargo_toml_files)?;
println!("\nFound {} unique dependencies:", dependencies.len());
if detailed {
display_detailed_dependency_info(project_path, &cargo_toml_files)?;
} else {
display_simple_dependency_list(&dependencies);
}
Ok(())
}
fn display_simple_dependency_list(dependencies: &HashSet<String>) {
let mut sorted_deps: Vec<_> = dependencies.iter().collect();
sorted_deps.sort();
for dep in sorted_deps {
println!("- {}", dep);
}
}
fn display_detailed_dependency_info(
project_path: &Path,
cargo_toml_files: &[PathBuf],
) -> Result<()> {
display_dependency_specs_by_file(cargo_toml_files)?;
display_cargo_lock_versions(project_path, cargo_toml_files)?;
Ok(())
}
fn display_dependency_specs_by_file(cargo_toml_files: &[PathBuf]) -> Result<()> {
for (index, cargo_toml) in cargo_toml_files.iter().enumerate() {
println!("\nDependency specifications from {}:", cargo_toml.display());
let dependency_info = extract_dependency_info(cargo_toml)?;
let mut sorted_info: Vec<_> = dependency_info.iter().collect();
sorted_info.sort_by(|a, b| a.version.cmp(&b.version));
for dep in sorted_info {
println!("{}: {}", dep.name, dep.version);
}
if index < cargo_toml_files.len() - 1 {
println!("\n---");
}
}
Ok(())
}
fn display_cargo_lock_versions(project_path: &Path, cargo_toml_files: &[PathBuf]) -> Result<()> {
if let Ok(cargo_lock_path) = find_cargo_lock(project_path) {
println!("\nFound Cargo.lock at: {}", cargo_lock_path.display());
let first_cargo_toml = &cargo_toml_files[0];
let dependency_info = extract_dependency_info(first_cargo_toml)?;
if let Ok(resolved_versions) =
resolve_dependency_versions(cargo_lock_path, &dependency_info)
{
println!("\nResolved dependency versions from Cargo.lock:");
let mut sorted_resolved: Vec<_> = resolved_versions.iter().collect();
sorted_resolved.sort_by(|a, b| a.version.cmp(&b.version));
for dep in sorted_resolved {
println!("{}: {}", dep.name, dep.version);
}
}
}
Ok(())
}