use crate::domain::packs::external_fetcher::ExternalFetcherFactory;
use crate::domain::packs::types::Pack;
use crate::utils::error::Result;
use flate2::read::GzDecoder;
use std::fs;
use std::path::{Path, PathBuf};
use tar::Archive;
pub struct InstallInput {
pub pack_id: String,
pub target_dir: Option<PathBuf>,
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallOutput {
pub pack_id: String,
pub pack_name: String,
pub packages_installed: Vec<String>,
pub templates_available: Vec<String>,
pub sparql_queries: usize,
pub total_packages: usize,
pub install_path: PathBuf,
}
pub async fn install_pack(input: &InstallInput) -> Result<InstallOutput> {
let pack = if input.pack_id.contains(':') {
fetch_external_pack(&input.pack_id).await?
} else {
crate::domain::packs::metadata::show_pack(&input.pack_id).map_err(|e| {
crate::utils::error::Error::new(&format!(
"Pack '{}' not found locally: {}",
input.pack_id, e
))
})?
};
let install_path = input.target_dir.clone().unwrap_or_else(|| {
dirs::home_dir()
.map(|p| p.join(".ggen").join("packs").join(&input.pack_id))
.unwrap_or_else(|| PathBuf::from(".ggen").join("packs").join(&input.pack_id))
});
if install_path.exists() && !input.force {
return Err(crate::utils::error::Error::new(&format!(
"Pack already installed at {}",
install_path.display()
)));
}
if !input.dry_run {
fs::create_dir_all(&install_path).map_err(|e| {
crate::utils::error::Error::new(&format!("Failed to create install dir: {}", e))
})?;
if input.pack_id.contains(':') {
download_and_verify_external_pack(&input.pack_id, &pack, &install_path).await?;
unpack_external_pack(&input.pack_id, &pack, &install_path).await?;
}
}
let packages_installed = pack.packages.clone();
Ok(InstallOutput {
pack_id: input.pack_id.clone(),
pack_name: pack.name,
packages_installed,
templates_available: pack.templates.iter().map(|t| t.name.clone()).collect(),
sparql_queries: pack.sparql_queries.len(),
total_packages: pack.packages.len(),
install_path,
})
}
async fn fetch_external_pack(pack_id: &str) -> Result<Pack> {
let (fetcher, remote_id) = ExternalFetcherFactory::get_fetcher_by_prefix(pack_id)?;
let remote_pkg = fetcher.fetch_metadata(&remote_id).await?;
Ok(Pack {
id: pack_id.to_string(),
name: remote_pkg.name.clone(),
version: remote_pkg.latest_version.clone(),
description: remote_pkg.description.unwrap_or_default(),
category: "external".to_string(),
author: None,
repository: remote_pkg.repository,
license: remote_pkg.license,
registry_type: Some(fetcher.registry_prefix().to_string()),
packages: vec![remote_pkg.name],
templates: vec![],
sparql_queries: std::collections::HashMap::new(),
dependencies: vec![],
tags: vec![],
keywords: vec![],
production_ready: true,
metadata: Default::default(),
})
}
async fn download_and_verify_external_pack(
pack_id: &str, pack: &Pack, install_path: &Path,
) -> Result<()> {
let (fetcher, remote_id) = ExternalFetcherFactory::get_fetcher_by_prefix(pack_id)?;
tracing::info!("Downloading artifact for {} v{}", pack_id, pack.version);
let artifact_bytes = fetcher.fetch_artifact(&remote_id, &pack.version).await?;
let artifact_path = install_path.join("artifact.tar.gz");
fs::write(&artifact_path, artifact_bytes).map_err(|e| {
crate::utils::error::Error::new(&format!("Failed to write artifact: {}", e))
})?;
Ok(())
}
async fn unpack_external_pack(_pack_id: &str, pack: &Pack, install_path: &Path) -> Result<()> {
let artifact_path = install_path.join("artifact.tar.gz");
if !artifact_path.exists() {
return Err(crate::utils::error::Error::new(
"Artifact not found for unpacking",
));
}
let file = fs::File::open(&artifact_path)
.map_err(|e| crate::utils::error::Error::new(&format!("Failed to open artifact: {}", e)))?;
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
archive.unpack(install_path).map_err(|e| {
crate::utils::error::Error::new(&format!("Failed to unpack artifact: {}", e))
})?;
let package_toml_path = install_path.join("package.toml");
let registry_type = match pack.registry_type.as_deref() {
Some("cratesio") | Some("crates.io") => "crates.io",
Some(other) => other,
None => "ggen",
};
let package_toml_content = format!(
r#"[package]
name = "{}"
version = "{}"
description = "{}"
license = "{}"
registry_type = "{}"
"#,
pack.name,
pack.version,
pack.description.replace('"', "\\\""),
pack.license.as_deref().unwrap_or("MIT"),
registry_type
);
fs::write(package_toml_path, package_toml_content).map_err(|e| {
crate::utils::error::Error::new(&format!("Failed to write package.toml: {}", e))
})?;
let _ = fs::remove_file(artifact_path);
Ok(())
}