use crate::github::GitHubSource;
use crate::package_manifest::PackageManifest;
use std::path::Path;
const PACKAGES_DIR: &str = "cufflink-packages";
pub async fn run(
source: &str,
api_url: Option<&str>,
tenant: Option<&str>,
_env: Option<&str>,
) -> eyre::Result<()> {
let (source_dir, _temp) = if source.starts_with("github:") {
let gh = GitHubSource::parse(source)?;
println!("Installing {}...", gh.display());
let temp = tempfile::tempdir()?;
crate::github::download(&gh, temp.path()).await?;
(temp.path().to_path_buf(), Some(temp))
} else {
let path = std::path::PathBuf::from(shellexpand::tilde(source).as_ref());
if !path.exists() {
eyre::bail!("Path '{}' does not exist", path.display());
}
println!("Installing from {}...", path.display());
(path, None)
};
let manifest = PackageManifest::load(&source_dir)?;
manifest.validate_paths(&source_dir)?;
let packages_dir = std::env::current_dir()?.join(PACKAGES_DIR);
let dest = packages_dir.join(&manifest.package.name);
if dest.exists() {
print!(
"Package '{}' already installed. Overwrite? [y/N] ",
manifest.package.name
);
use std::io::Write;
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Aborted.");
return Ok(());
}
std::fs::remove_dir_all(&dest)?;
}
std::fs::create_dir_all(&packages_dir)?;
let output = std::process::Command::new("cp")
.args(["-r", &source_dir.to_string_lossy(), &dest.to_string_lossy()])
.output()?;
if !output.status.success() {
eyre::bail!("Failed to copy package to {}", dest.display());
}
let api_url = api_url
.map(|s| s.to_string())
.or_else(|| std::env::var("CUFFLINK_API_URL").ok());
let tenant = tenant
.map(|s| s.to_string())
.or_else(|| std::env::var("CUFFLINK_TENANT").ok());
if let (Some(url), Some(slug)) = (&api_url, &tenant) {
write_env_config(&manifest, &dest, url, slug)?;
println!("Configured with api_url={} tenant={}", url, slug);
} else {
println!("No --api-url/--tenant provided. Configure each component before deploying.");
println!("See the package README for setup instructions:");
println!(" cat {}/{}/README.md", PACKAGES_DIR, manifest.package.name);
println!();
println!("Add to each component's Cufflink.toml:");
println!();
println!(" [environments.local]");
println!(" api_url = \"http://localhost:8080\"");
println!(" tenant = \"default\"");
}
update_lockfile(&packages_dir, &manifest, source)?;
println!();
manifest.print_summary();
println!("Installed to {}/{}/", PACKAGES_DIR, manifest.package.name);
println!();
println!("Deploy with:");
println!(" cufflink deploy-package {}", manifest.package.name);
Ok(())
}
fn write_env_config(
manifest: &PackageManifest,
package_dir: &Path,
api_url: &str,
tenant: &str,
) -> eyre::Result<()> {
let env_block = format!(
"\n\n[environments.local]\napi_url = \"{}\"\ntenant = \"{}\"\n",
api_url, tenant
);
let component_paths = manifest
.services
.iter()
.map(|s| s.path.as_str())
.chain(manifest.frontends.iter().map(|f| f.path.as_str()));
for component_path in component_paths {
let toml_path = package_dir.join(component_path).join("Cufflink.toml");
if !toml_path.exists() {
continue;
}
let content = std::fs::read_to_string(&toml_path)?;
if content.contains("[environments.") {
continue;
}
let mut new_content = content.trim_end().to_string();
if !new_content.contains("default_env") {
new_content =
new_content.replacen("[service]\n", "[service]\ndefault_env = \"local\"\n", 1);
}
new_content.push_str(&env_block);
std::fs::write(&toml_path, new_content)?;
}
Ok(())
}
fn update_lockfile(
packages_dir: &Path,
manifest: &PackageManifest,
source: &str,
) -> eyre::Result<()> {
let lockfile = packages_dir.join("cufflink-packages.toml");
let mut content = std::fs::read_to_string(&lockfile).unwrap_or_default();
let version = manifest.package.version.as_deref().unwrap_or("0.0.0");
let entry = format!(
"\n[{}]\nversion = \"{}\"\nsource = \"{}\"\ninstalled_at = \"{}\"\n",
manifest.package.name,
version,
source,
chrono::Utc::now().to_rfc3339(),
);
if let Some(start) = content.find(&format!("[{}]", manifest.package.name)) {
let end = content[start + 1..]
.find("\n[")
.map(|i| start + 1 + i)
.unwrap_or(content.len());
content.replace_range(start..end, "");
}
content.push_str(&entry);
std::fs::write(&lockfile, content.trim_start())?;
Ok(())
}