use crate::github::GitHubSource;
use crate::package_manifest::PackageManifest;
use comfy_table::{presets::NOTHING, Table, TableComponent};
use std::path::Path;
const PACKAGES_DIR: &str = "cufflink-packages";
pub async fn run(source: &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());
}
inject_environments(&manifest, &dest)?;
update_lockfile(&packages_dir, &manifest, source)?;
println!();
manifest.print_summary();
println!("Installed to {}/{}/", PACKAGES_DIR, manifest.package.name);
println!();
print!("Deploy all components? [y/N] ");
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") {
deploy_package(&manifest, &dest, env).await?;
} else {
println!("Skipped deploy. Deploy later from each component directory:");
for s in &manifest.services {
println!(
" cd {}/{}/{} && cufflink deploy",
PACKAGES_DIR, manifest.package.name, s.path
);
}
for f in &manifest.frontends {
println!(
" cd {}/{}/{} && cufflink deploy",
PACKAGES_DIR, manifest.package.name, f.path
);
}
}
Ok(())
}
fn inject_environments(manifest: &PackageManifest, package_dir: &Path) -> eyre::Result<()> {
let parent_config = crate::project_config::ProjectConfig::find_and_load()?;
let parent = match parent_config {
Some(p) if !p.environments.is_empty() => p,
_ => return Ok(()),
};
let parent_toml_path = parent.project_dir.join("Cufflink.toml");
let parent_content = std::fs::read_to_string(&parent_toml_path)?;
let env_sections = extract_environment_sections(&parent_content);
if env_sections.is_empty() {
return Ok(());
}
let component_paths: Vec<&str> = manifest
.services
.iter()
.map(|s| s.path.as_str())
.chain(manifest.frontends.iter().map(|f| f.path.as_str()))
.collect();
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();
new_content.push_str("\n\n");
new_content.push_str(&env_sections);
new_content.push('\n');
std::fs::write(&toml_path, new_content)?;
}
for component_path in manifest
.services
.iter()
.map(|s| s.path.as_str())
.chain(manifest.frontends.iter().map(|f| f.path.as_str()))
{
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("default_env") {
if let Some(default_env) = &parent.service.default_env {
let updated = content.replacen(
"[service]\n",
&format!("[service]\ndefault_env = \"{}\"\n", default_env),
1,
);
std::fs::write(&toml_path, updated)?;
}
}
}
Ok(())
}
fn extract_environment_sections(toml_content: &str) -> String {
let mut result = String::new();
let mut in_env_section = false;
for line in toml_content.lines() {
if line.starts_with("[environments.") || line.starts_with("[environments]") {
in_env_section = true;
} else if line.starts_with('[') && !line.starts_with("[environments") {
in_env_section = false;
}
if in_env_section {
result.push_str(line);
result.push('\n');
}
}
result
}
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(())
}
async fn deploy_package(
manifest: &PackageManifest,
package_dir: &Path,
env: Option<&str>,
) -> eyre::Result<()> {
if !manifest.config.required.is_empty() {
println!("Note: Set these config keys after deployment:");
for key in &manifest.config.required {
println!(" cufflink config set {} <value>", key);
}
println!();
}
let original_dir = std::env::current_dir()?;
let mut results: Vec<(&str, &str, Result<(), String>)> = Vec::new();
for service in &manifest.services {
let service_dir = package_dir.join(&service.path);
println!("Deploying service '{}'...", service.name);
std::env::set_current_dir(&service_dir)?;
match super::deploy::run(false, None, env).await {
Ok(_) => results.push((&service.name, "service", Ok(()))),
Err(e) => {
let msg = format!("{}", e);
eprintln!(" Failed: {}", msg);
results.push((&service.name, "service", Err(msg)));
}
}
}
for frontend in &manifest.frontends {
let frontend_dir = package_dir.join(&frontend.path);
println!("Deploying frontend '{}'...", frontend.name);
std::env::set_current_dir(&frontend_dir)?;
match super::deploy::run(false, None, env).await {
Ok(_) => results.push((&frontend.name, "frontend", Ok(()))),
Err(e) => {
let msg = format!("{}", e);
eprintln!(" Failed: {}", msg);
results.push((&frontend.name, "frontend", Err(msg)));
}
}
}
std::env::set_current_dir(&original_dir)?;
println!();
let mut table = Table::new();
table.load_preset(NOTHING);
table.set_style(TableComponent::HeaderLines, '-');
table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
table.set_header(vec!["COMPONENT", "TYPE", "STATUS"]);
let mut failures = 0;
for (name, kind, result) in &results {
let status = match result {
Ok(_) => "deployed",
Err(_) => {
failures += 1;
"FAILED"
}
};
table.add_row(vec![*name, *kind, status]);
}
println!("{table}");
if failures > 0 {
eyre::bail!("{} component(s) failed to deploy", failures);
}
Ok(())
}