use clap::Subcommand;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tusktsk::TuskResult as Result;
use tracing::info;
use tokio::process::Command;
use std::collections::HashMap;
use tusktsk::TuskError;
#[derive(Subcommand)]
pub enum DependencyCommand {
Install {
#[arg(short, long)]
package: Option<String>,
#[arg(long, default_value = "all")]
group: String,
#[arg(long)]
version: Option<String>,
#[arg(long)]
global: bool,
#[arg(long)]
force: bool,
#[arg(long)]
no_deps: bool,
#[arg(long, default_value = "auto")]
manager: String,
},
List {
#[arg(long)]
group: Option<String>,
#[arg(long)]
outdated: bool,
#[arg(short, long)]
verbose: bool,
#[arg(long, default_value = "table")]
format: String,
#[arg(long)]
filter: Option<String>,
},
Check {
#[arg(short, long)]
package: Option<String>,
#[arg(long)]
all: bool,
#[arg(long)]
security: bool,
#[arg(long)]
licenses: bool,
#[arg(long)]
updates: bool,
#[arg(long, default_value = "text")]
format: String,
#[arg(long)]
report: Option<PathBuf>,
},
Update {
#[arg(short, long)]
package: Option<String>,
#[arg(long)]
all: bool,
#[arg(long)]
latest: bool,
#[arg(long)]
version: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(short, long)]
interactive: bool,
},
Remove {
#[arg(short, long)]
package: String,
#[arg(long)]
unused: bool,
#[arg(long)]
global: bool,
#[arg(long)]
force: bool,
#[arg(long)]
keep_config: bool,
},
Search {
#[arg(short, long)]
query: String,
#[arg(long)]
group: Option<String>,
#[arg(short, long)]
verbose: bool,
#[arg(long, default_value = "20")]
limit: usize,
#[arg(long, default_value = "downloads")]
sort: String,
},
Info {
#[arg(short, long)]
package: String,
#[arg(long)]
versions: bool,
#[arg(long)]
deps: bool,
#[arg(long)]
reverse: bool,
#[arg(long)]
security: bool,
},
}
#[derive(Debug, Serialize, Deserialize)]
struct DependencyConfig {
groups: HashMap<String, DependencyGroup>,
package_managers: Vec<PackageManager>,
default_manager: String,
auto_update: bool,
security_checks: bool,
license_checks: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct DependencyGroup {
name: String,
description: String,
packages: Vec<String>,
required: bool,
category: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct PackageManager {
name: String,
command: String,
install_cmd: String,
list_cmd: String,
update_cmd: String,
remove_cmd: String,
search_cmd: String,
info_cmd: String,
enabled: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct Package {
name: String,
version: String,
latest_version: Option<String>,
description: Option<String>,
group: String,
manager: String,
installed: bool,
outdated: bool,
dependencies: Vec<String>,
reverse_dependencies: Vec<String>,
license: Option<String>,
security_issues: Vec<SecurityIssue>,
size: Option<u64>,
install_date: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SecurityIssue {
severity: String,
description: String,
cve_id: Option<String>,
affected_version: String,
fixed_version: Option<String>,
advisory_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DependencyCheckResult {
package: String,
status: String,
issues: Vec<String>,
recommendations: Vec<String>,
security_vulnerabilities: Vec<SecurityIssue>,
license_issues: Vec<String>,
update_available: bool,
latest_version: Option<String>,
}
pub async fn run(cmd: DependencyCommand) -> Result<()> {
match cmd {
DependencyCommand::Install { package, group, version, global, force, no_deps, manager } => {
install_dependencies(package, group, version, global, force, no_deps, manager).await
}
DependencyCommand::List { group, outdated, verbose, format, filter } => {
list_dependencies(group, outdated, verbose, format, filter).await
}
DependencyCommand::Check { package, all, security, licenses, updates, format, report } => {
check_dependencies(package, all, security, licenses, updates, format, report).await
}
DependencyCommand::Update { package, all, latest, version, dry_run, interactive } => {
update_dependencies(package, all, latest, version, dry_run, interactive).await
}
DependencyCommand::Remove { package, unused, global, force, keep_config } => {
remove_dependencies(package, unused, global, force, keep_config).await
}
DependencyCommand::Search { query, group, verbose, limit, sort } => {
search_packages(query, group, verbose, limit, sort).await
}
DependencyCommand::Info { package, versions, deps, reverse, security } => {
show_package_info(package, versions, deps, reverse, security).await
}
}
}
async fn install_dependencies(
package: Option<String>,
group: String,
version: Option<String>,
global: bool,
force: bool,
no_deps: bool,
manager: String,
) -> Result<()> {
info!("📦 Installing dependencies...");
let config = load_dependency_config().await?;
if let Some(pkg) = package {
install_single_package(&pkg, &version, global, force, no_deps, &manager).await?;
} else {
install_group_packages(&group, global, force, no_deps, &manager).await?;
}
println!("✅ Dependencies installed successfully");
Ok(())
}
async fn list_dependencies(
group: Option<String>,
outdated: bool,
verbose: bool,
format: String,
filter: Option<String>,
) -> Result<()> {
info!("📋 Listing dependencies...");
let packages = get_installed_packages().await?;
let mut filtered_packages = packages;
if let Some(group_name) = group {
filtered_packages = filtered_packages
.into_iter()
.filter(|p| p.group == group_name)
.collect();
}
if let Some(filter_name) = filter {
filtered_packages = filtered_packages
.into_iter()
.filter(|p| p.name.contains(&filter_name))
.collect();
}
if outdated {
filtered_packages = filtered_packages
.into_iter()
.filter(|p| p.outdated)
.collect();
}
match format.as_str() {
"table" => print_packages_table(&filtered_packages, verbose),
"json" => println!("{}", serde_json::to_string_pretty(&filtered_packages)?),
"yaml" => println!("{}", serde_yaml::to_string(&filtered_packages)?),
_ => return Err(TuskError::parse_error(0, format!("Unknown output format: {}", format))),
}
Ok(())
}
async fn check_dependencies(
package: Option<String>,
all: bool,
security: bool,
licenses: bool,
updates: bool,
format: String,
report: Option<PathBuf>,
) -> Result<()> {
info!("🔍 Checking dependencies...");
let mut results = Vec::new();
if let Some(pkg) = package {
let result = check_single_package(&pkg, security, licenses, updates).await?;
results.push(result);
} else if all {
let packages = get_installed_packages().await?;
for pkg in packages {
let result = check_single_package(&pkg.name, security, licenses, updates).await?;
results.push(result);
}
} else {
return Err(TuskError::parse_error(0, "Please specify a package or use --all".to_string()));
}
let report_data = serde_json::to_string_pretty(&results)?;
if let Some(report_path) = report {
tokio::fs::write(&report_path, report_data).await?;
println!("📄 Dependency check report saved to: {:?}", report_path);
} else {
match format.as_str() {
"text" => print_check_results(&results),
"json" => println!("{}", report_data),
"yaml" => println!("{}", serde_yaml::to_string(&results)?),
_ => return Err(TuskError::parse_error(0, format!("Unknown output format: {}", format))),
}
}
Ok(())
}
async fn update_dependencies(
package: Option<String>,
all: bool,
latest: bool,
version: Option<String>,
dry_run: bool,
interactive: bool,
) -> Result<()> {
info!("🔄 Updating dependencies...");
if dry_run {
println!("🔍 Dry run mode - showing what would be updated");
}
if let Some(pkg) = package {
update_single_package(&pkg, latest, &version, dry_run, interactive).await?;
} else if all {
let packages = get_installed_packages().await?;
for pkg in packages {
if pkg.outdated {
update_single_package(&pkg.name, latest, &version, dry_run, interactive).await?;
}
}
} else {
return Err(TuskError::parse_error(0, "Please specify a package or use --all".to_string()));
}
if !dry_run {
println!("✅ Dependencies updated successfully");
}
Ok(())
}
async fn remove_dependencies(
package: String,
unused: bool,
global: bool,
force: bool,
keep_config: bool,
) -> Result<()> {
info!("🗑️ Removing dependencies...");
if unused {
let unused_packages = find_unused_packages().await?;
for pkg in &unused_packages {
remove_single_package(pkg, global, force, keep_config).await?;
}
println!("✅ Unused dependencies removed");
} else {
remove_single_package(&package, global, force, keep_config).await?;
println!("✅ Package '{}' removed successfully", package);
}
Ok(())
}
async fn search_packages(
query: String,
group: Option<String>,
verbose: bool,
limit: usize,
sort: String,
) -> Result<()> {
info!("🔍 Searching packages...");
println!("🔍 Searching for: {}", query);
let results = search_package_registry(&query, &group, limit, &sort).await?;
if results.is_empty() {
println!("❌ No packages found matching '{}'", query);
} else {
println!("📦 Found {} packages:", results.len());
for (i, pkg) in results.iter().take(limit).enumerate() {
println!("{}. {} ({})", i + 1, pkg.name, pkg.version);
if verbose {
if let Some(desc) = &pkg.description {
println!(" Description: {}", desc);
}
println!(" Group: {}", pkg.group);
println!(" Manager: {}", pkg.manager);
}
println!();
}
}
Ok(())
}
async fn show_package_info(
package: String,
versions: bool,
deps: bool,
reverse: bool,
security: bool,
) -> Result<()> {
info!("📋 Showing package information...");
let pkg_info = get_package_info(&package).await?;
println!("📦 Package: {}", pkg_info.name);
println!("📋 Version: {}", pkg_info.version);
if let Some(desc) = &pkg_info.description {
println!("📝 Description: {}", desc);
}
println!("📁 Group: {}", pkg_info.group);
println!("🔧 Manager: {}", pkg_info.manager);
println!("✅ Installed: {}", pkg_info.installed);
if pkg_info.outdated {
if let Some(latest) = &pkg_info.latest_version {
println!("🔄 Outdated: {} (latest: {})", pkg_info.version, latest);
}
}
if let Some(license) = &pkg_info.license {
println!("📄 License: {}", license);
}
if let Some(size) = pkg_info.size {
println!("📊 Size: {} bytes", size);
}
if let Some(install_date) = pkg_info.install_date {
println!("📅 Installed: {}", install_date.format("%Y-%m-%d %H:%M:%S"));
}
if versions {
println!("\n📋 Available versions:");
println!(" (Version listing not implemented)");
}
if deps && !pkg_info.dependencies.is_empty() {
println!("\n📦 Dependencies:");
for dep in &pkg_info.dependencies {
println!(" - {}", dep);
}
}
if reverse && !pkg_info.reverse_dependencies.is_empty() {
println!("\n🔄 Reverse dependencies:");
for dep in &pkg_info.reverse_dependencies {
println!(" - {}", dep);
}
}
if security && !pkg_info.security_issues.is_empty() {
println!("\n🚨 Security issues:");
for issue in &pkg_info.security_issues {
println!(" - [{}] {}", issue.severity.to_uppercase(), issue.description);
if let Some(cve) = &issue.cve_id {
println!(" CVE: {}", cve);
}
if let Some(fixed) = &issue.fixed_version {
println!(" Fixed in: {}", fixed);
}
}
}
Ok(())
}
async fn load_dependency_config() -> Result<DependencyConfig> {
let config_path = PathBuf::from("/etc/tsk/dependencies.json");
if config_path.exists() {
let content = tokio::fs::read_to_string(&config_path).await?;
Ok(serde_json::from_str::<DependencyConfig>(&content)?)
} else {
let mut groups = HashMap::new();
groups.insert("core".to_string(), DependencyGroup {
name: "core".to_string(),
description: "Core system dependencies".to_string(),
packages: vec!["serde".to_string(), "tokio".to_string(), "anyhow".to_string()],
required: true,
category: "system".to_string(),
});
groups.insert("web".to_string(), DependencyGroup {
name: "web".to_string(),
description: "Web framework dependencies".to_string(),
packages: vec!["actix-web".to_string(), "reqwest".to_string()],
required: false,
category: "web".to_string(),
});
groups.insert("security".to_string(), DependencyGroup {
name: "security".to_string(),
description: "Security and cryptography dependencies".to_string(),
packages: vec!["sha2".to_string(), "argon2".to_string(), "jsonwebtoken".to_string()],
required: false,
category: "security".to_string(),
});
groups.insert("ai".to_string(), DependencyGroup {
name: "ai".to_string(),
description: "AI and machine learning dependencies".to_string(),
packages: vec!["tch".to_string(), "rust-bert".to_string()],
required: false,
category: "ai".to_string(),
});
groups.insert("database".to_string(), DependencyGroup {
name: "database".to_string(),
description: "Database and storage dependencies".to_string(),
packages: vec!["sqlx".to_string(), "redis".to_string(), "mongodb".to_string()],
required: false,
category: "database".to_string(),
});
groups.insert("cache".to_string(), DependencyGroup {
name: "cache".to_string(),
description: "Caching and performance dependencies".to_string(),
packages: vec!["memcached".to_string(), "dashmap".to_string()],
required: false,
category: "cache".to_string(),
});
groups.insert("monitoring".to_string(), DependencyGroup {
name: "monitoring".to_string(),
description: "Monitoring and observability dependencies".to_string(),
packages: vec!["prometheus".to_string(), "opentelemetry".to_string()],
required: false,
category: "monitoring".to_string(),
});
groups.insert("utils".to_string(), DependencyGroup {
name: "utils".to_string(),
description: "Utility and helper dependencies".to_string(),
packages: vec!["chrono".to_string(), "uuid".to_string(), "base64".to_string()],
required: false,
category: "utils".to_string(),
});
Ok(DependencyConfig {
groups,
package_managers: vec![
PackageManager {
name: "cargo".to_string(),
command: "cargo".to_string(),
install_cmd: "add".to_string(),
list_cmd: "tree".to_string(),
update_cmd: "update".to_string(),
remove_cmd: "remove".to_string(),
search_cmd: "search".to_string(),
info_cmd: "search".to_string(),
enabled: true,
}
],
default_manager: "cargo".to_string(),
auto_update: false,
security_checks: true,
license_checks: true,
})
}
}
async fn install_single_package(
package: &str,
version: &Option<String>,
global: bool,
force: bool,
no_deps: bool,
manager: &str,
) -> Result<()> {
let config = load_dependency_config().await?;
let pkg_manager = config.package_managers
.iter()
.find(|pm| pm.name == manager)
.ok_or_else(|| TuskError::parse_error(0, format!("Package manager not found: {}", manager)))?;
let mut cmd = Command::new(&pkg_manager.command);
cmd.arg(&pkg_manager.install_cmd);
cmd.arg(package);
if let Some(ver) = version {
cmd.arg(&format!("--version={}", ver));
}
if global {
cmd.arg("--global");
}
if force {
cmd.arg("--force");
}
if no_deps {
cmd.arg("--no-deps");
}
let output = cmd.output().await?;
if output.status.success() {
println!("✅ Installed package: {}", package);
} else {
let error = String::from_utf8_lossy(&output.stderr);
return Err(TuskError::parse_error(0, format!("Failed to install package: {}", error)));
}
Ok(())
}
async fn install_group_packages(
group: &str,
global: bool,
force: bool,
no_deps: bool,
manager: &str,
) -> Result<()> {
let config = load_dependency_config().await?;
let group_config = config.groups.get(group)
.ok_or_else(|| TuskError::parse_error(0, format!("Group not found: {}", group)))?;
println!("📦 Installing {} packages from group '{}'", group_config.packages.len(), group);
for package in &group_config.packages {
install_single_package(package, &None, global, force, no_deps, manager).await?;
}
Ok(())
}
async fn get_installed_packages() -> Result<Vec<Package>> {
Ok(vec![
Package {
name: "serde".to_string(),
version: "1.0.0".to_string(),
latest_version: Some("1.0.1".to_string()),
description: Some("Serialization framework".to_string()),
group: "core".to_string(),
manager: "cargo".to_string(),
installed: true,
outdated: true,
dependencies: vec![],
reverse_dependencies: vec![],
license: Some("MIT".to_string()),
security_issues: vec![],
size: Some(1024),
install_date: Some(chrono::Utc::now()),
}
])
}
fn print_packages_table(packages: &[Package], verbose: bool) {
if packages.is_empty() {
println!("📦 No packages found");
return;
}
println!("{:<20} {:<15} {:<10} {:<10} {:<10}", "Package", "Version", "Group", "Manager", "Status");
println!("{:-<70}", "");
for pkg in packages {
let status = if pkg.outdated { "🔄" } else { "✅" };
println!("{:<20} {:<15} {:<10} {:<10} {}",
pkg.name, pkg.version, pkg.group, pkg.manager, status);
if verbose {
if let Some(desc) = &pkg.description {
println!(" Description: {}", desc);
}
if pkg.outdated {
if let Some(latest) = &pkg.latest_version {
println!(" Latest version: {}", latest);
}
}
println!();
}
}
}
async fn check_single_package(
package: &str,
security: bool,
licenses: bool,
updates: bool,
) -> Result<DependencyCheckResult> {
Ok(DependencyCheckResult {
package: package.to_string(),
status: "ok".to_string(),
issues: vec![],
recommendations: vec![],
security_vulnerabilities: vec![],
license_issues: vec![],
update_available: false,
latest_version: None,
})
}
fn print_check_results(results: &[DependencyCheckResult]) {
println!("🔍 Dependency Check Results");
println!();
for result in results {
println!("📦 Package: {}", result.package);
println!("📋 Status: {}", result.status);
if !result.issues.is_empty() {
println!("🚨 Issues:");
for issue in &result.issues {
println!(" - {}", issue);
}
}
if !result.recommendations.is_empty() {
println!("💡 Recommendations:");
for rec in &result.recommendations {
println!(" - {}", rec);
}
}
if result.update_available {
println!("🔄 Update available");
if let Some(latest) = &result.latest_version {
println!(" Latest version: {}", latest);
}
}
println!();
}
}
async fn update_single_package(
package: &str,
latest: bool,
version: &Option<String>,
dry_run: bool,
interactive: bool,
) -> Result<()> {
if dry_run {
println!("🔍 Would update package: {}", package);
return Ok(());
}
println!("🔄 Updated package: {}", package);
Ok(())
}
async fn find_unused_packages() -> Result<Vec<String>> {
Ok(vec![])
}
async fn remove_single_package(
package: &str,
global: bool,
force: bool,
keep_config: bool,
) -> Result<()> {
println!("🗑️ Removed package: {}", package);
Ok(())
}
async fn search_package_registry(
query: &str,
group: &Option<String>,
limit: usize,
sort: &str,
) -> Result<Vec<Package>> {
Ok(vec![])
}
async fn get_package_info(package: &str) -> Result<Package> {
Ok(Package {
name: package.to_string(),
version: "1.0.0".to_string(),
latest_version: Some("1.0.1".to_string()),
description: Some("Package description".to_string()),
group: "core".to_string(),
manager: "cargo".to_string(),
installed: true,
outdated: true,
dependencies: vec![],
reverse_dependencies: vec![],
license: Some("MIT".to_string()),
security_issues: vec![],
size: Some(1024),
install_date: Some(chrono::Utc::now()),
})
}