use ggen_utils::error::Result;
#[derive(Debug, Clone, Default)]
pub struct UpdateInput {
pub package: Option<String>,
pub all: bool,
pub dry_run: bool,
}
pub async fn update_and_report(package: Option<&str>, all: bool, dry_run: bool) -> Result<()> {
let packages_dir = dirs::home_dir()
.ok_or_else(|| {
ggen_utils::error::Error::with_context("home directory not found", "~/.ggen/packages")
})?
.join(".ggen")
.join("packages");
let lockfile_path = packages_dir.join("ggen.lock");
if !lockfile_path.exists() {
ggen_utils::alert_info!("No packages installed.");
return Ok(());
}
let content = tokio::fs::read_to_string(&lockfile_path)
.await
.map_err(|e| ggen_utils::error::Error::new(&format!("IO error: {}", e)))?;
let lockfile: Lockfile = serde_json::from_str(&content)?;
if lockfile.packages.is_empty() {
ggen_utils::alert_info!("No packages installed.");
return Ok(());
}
let packages_to_update: Vec<_> = if let Some(pkg_name) = package {
if !lockfile.packages.contains_key(pkg_name) {
return Err(ggen_utils::error::Error::with_context(
&format!("Package {} is not installed", pkg_name),
"update",
));
}
vec![pkg_name.to_string()]
} else if all {
lockfile.packages.keys().cloned().collect()
} else {
return Err(ggen_utils::error::Error::with_context(
"Please specify a package name or use --all to update all packages",
"update",
));
};
if dry_run {
ggen_utils::alert_info!(
"🔍 Dry run: Would update {} package(s):",
packages_to_update.len()
);
for pkg_name in &packages_to_update {
if let Some(info) = lockfile.packages.get(pkg_name) {
ggen_utils::alert_info!(" {} (current: {})", pkg_name, info.version);
}
}
return Ok(());
}
let registry_path = dirs::home_dir()
.ok_or_else(|| {
ggen_utils::error::Error::with_context("home directory not found", "~/.ggen/registry")
})?
.join(".ggen")
.join("registry");
ggen_utils::alert_info!("🔄 Updating {} package(s)...\n", packages_to_update.len());
let updated_count: usize = 0;
let mut skipped_count = 0;
for pkg_name in &packages_to_update {
let current_version = lockfile.packages.get(pkg_name).map(|i| &i.version);
match check_for_updates(®istry_path, pkg_name, current_version).await? {
UpdateStatus::Available(new_version) => {
ggen_utils::alert_info!("📦 Updating {} to version {}...", pkg_name, new_version);
let lockfile_backup = lockfile.clone();
use super::install::install_and_report;
let pkg_spec = format!("{}@{}", pkg_name, new_version);
match install_and_report(&pkg_spec, None, true, false, false).await {
Ok(()) => {
ggen_utils::alert_success!("Updated {} to {}", pkg_name, new_version);
}
Err(e) => {
tracing::warn!(
"Update failed for {}: {}. Rolling back to previous state.",
pkg_name,
e
);
let backup_content = serde_json::to_string_pretty(&lockfile_backup)?;
tokio::fs::write(&lockfile_path, backup_content).await?;
let msg = format!(
"Failed to update {}: {}. Rolled back to previous version.",
pkg_name, e
);
ggen_utils::alert_critical!(&msg);
}
}
}
UpdateStatus::CompatibilityWarning(new_version, reason) => {
ggen_utils::alert_warning!(
"⚠️ Version update available for {} but with compatibility warning: {}",
pkg_name,
reason
);
ggen_utils::alert_info!(
" New version: {}. Manual review recommended before updating.",
new_version
);
skipped_count += 1;
}
UpdateStatus::UpToDate => {
ggen_utils::alert_info!("✓ {} is up to date", pkg_name);
skipped_count += 1;
}
UpdateStatus::NotFound => {
ggen_utils::alert_warning!("{} not found in registry", pkg_name);
skipped_count += 1;
}
}
}
ggen_utils::alert_info!(
"Summary: {} updated, {} skipped",
updated_count,
skipped_count
);
Ok(())
}
pub async fn execute_update(input: UpdateInput) -> Result<UpdateOutput> {
use ggen_marketplace::RdfRegistry;
let _registry_path = dirs::home_dir()
.ok_or_else(|| ggen_utils::error::Error::new("home directory not found"))?
.join(".ggen")
.join("registry");
let _registry = RdfRegistry::new();
let packages_dir = dirs::home_dir()
.ok_or_else(|| ggen_utils::error::Error::new("home directory not found"))?
.join(".ggen")
.join("packages");
let lockfile_path = packages_dir.join("ggen.lock");
if !lockfile_path.exists() {
return Ok(UpdateOutput {
packages_updated: 0,
});
}
let content = tokio::fs::read_to_string(&lockfile_path).await?;
let lockfile: Lockfile = serde_json::from_str(&content)?;
if lockfile.packages.is_empty() {
return Ok(UpdateOutput {
packages_updated: 0,
});
}
let packages_to_update: Vec<_> = if let Some(ref pkg_name) = input.package {
if !lockfile.packages.contains_key(pkg_name) {
return Err(ggen_utils::error::Error::new(&format!(
"Package {} is not installed",
pkg_name
)));
}
vec![pkg_name.clone()]
} else if input.all {
lockfile.packages.keys().cloned().collect()
} else {
return Err(ggen_utils::error::Error::new(
"Please specify a package name or use --all",
));
};
if input.dry_run {
ggen_utils::alert_info!(
"🔍 Dry run: Would update {} package(s)",
packages_to_update.len()
);
return Ok(UpdateOutput {
packages_updated: 0,
});
}
let updated_count: usize = 0;
for pkg_name in &packages_to_update {
let Some(_pkg_info) = lockfile.packages.get(pkg_name) else {
ggen_utils::alert_warning!("Missing lockfile entry for: {}", pkg_name);
continue;
};
ggen_utils::alert_info!(
"📦 Package {} - v2 marketplace implementation pending",
pkg_name
);
}
Ok(UpdateOutput {
packages_updated: updated_count,
})
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct UpdateOutput {
pub packages_updated: usize,
}
enum UpdateStatus {
Available(String), CompatibilityWarning(String, String), UpToDate, NotFound, }
async fn check_for_updates(
registry_path: &std::path::Path, package_name: &str, current_version: Option<&String>,
) -> Result<UpdateStatus> {
let index_path = registry_path.join("index.json");
if !index_path.exists() {
return Ok(UpdateStatus::NotFound);
}
let metadata = tokio::fs::metadata(&index_path).await.ok();
if let Some(meta) = metadata {
if let Ok(modified) = meta.modified() {
use std::time::SystemTime;
if let Ok(duration) = SystemTime::now().duration_since(modified) {
let days_old = duration.as_secs() / (24 * 3600);
if days_old > 7 {
tracing::warn!(
"Registry index is {} days old. Consider running 'ggen marketplace sync' for latest packages",
days_old
);
}
}
}
}
let content = tokio::fs::read_to_string(&index_path)
.await
.map_err(|e| ggen_utils::error::Error::new(&format!("IO error: {}", e)))?;
let index: serde_json::Value = serde_json::from_str(&content)?;
let versions = index
.get("packages")
.and_then(|p| p.get(package_name))
.and_then(|v| v.as_array());
match versions {
Some(versions) if !versions.is_empty() => {
let latest = &versions[0];
let latest_version =
latest
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ggen_utils::error::Error::with_context(
"Invalid registry index format",
"check_for_updates",
)
})?;
match current_version {
Some(current) if current == latest_version => Ok(UpdateStatus::UpToDate),
Some(current) => {
match check_version_compatibility(current, latest_version) {
Ok(true) => Ok(UpdateStatus::Available(latest_version.to_string())),
Ok(false) => {
tracing::warn!(
"Version {} to {} may break compatibility. Manual review recommended.",
current, latest_version
);
Ok(UpdateStatus::CompatibilityWarning(
latest_version.to_string(),
"Version change may break compatibility".to_string(),
))
}
Err(e) => {
tracing::warn!(
"Could not verify compatibility: {}. Proceeding with caution.",
e
);
Ok(UpdateStatus::Available(latest_version.to_string()))
}
}
}
None => Ok(UpdateStatus::Available(latest_version.to_string())),
}
}
_ => Ok(UpdateStatus::NotFound),
}
}
fn check_version_compatibility(current: &str, latest: &str) -> Result<bool> {
let current_parts: Vec<&str> = current.split('.').collect();
let latest_parts: Vec<&str> = latest.split('.').collect();
if current_parts.len() < 3 || latest_parts.len() < 3 {
return Ok(true);
}
let current_major = current_parts[0].parse::<u32>().unwrap_or(0);
let latest_major = latest_parts[0].parse::<u32>().unwrap_or(0);
if latest_major < current_major {
return Ok(false);
}
Ok(true)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Lockfile {
#[serde(default)]
version: String,
#[serde(default)]
packages: std::collections::HashMap<String, PackageInfo>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct PackageInfo {
version: String,
#[serde(skip_serializing_if = "Option::is_none")]
installed_at: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_update_dry_run_no_lockfile() {
let result = update_and_report(None, true, true).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_no_args() {
let result = update_and_report(None, false, false).await;
assert!(result.is_ok());
}
#[test]
fn test_lockfile_deserialization() {
let lockfile_json = r#"{
"version": "1.0",
"packages": {
"test/example": {
"version": "1.0.0",
"installed_at": "2024-01-01T00:00:00Z"
}
}
}"#;
let lockfile: Lockfile = serde_json::from_str(lockfile_json).unwrap();
assert_eq!(lockfile.packages.len(), 1);
}
#[test]
fn test_version_compatibility_patch_update() {
let result = check_version_compatibility("1.0.0", "1.0.1");
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_version_compatibility_minor_update() {
let result = check_version_compatibility("1.0.0", "1.1.0");
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_version_compatibility_major_update() {
let result = check_version_compatibility("1.0.0", "2.0.0");
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_version_compatibility_major_downgrade() {
let result = check_version_compatibility("2.0.0", "1.0.0");
assert_eq!(result.unwrap(), false);
}
#[test]
fn test_version_compatibility_invalid_versions() {
let result = check_version_compatibility("1.0", "2.0.0");
assert_eq!(result.unwrap(), true);
}
}