use anyhow::{anyhow, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeployTarget {
Docker,
Standalone,
K8s,
}
impl DeployTarget {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"docker" | "container" => Ok(Self::Docker),
"standalone" | "binary" | "sea" => Ok(Self::Standalone),
"k8s" | "kubernetes" => Ok(Self::K8s),
_ => Err(anyhow!(
"Unknown deploy target: '{}'. Valid: docker, standalone, k8s",
s
)),
}
}
}
pub struct DeployOptions {
pub target: DeployTarget,
pub output_dir: PathBuf,
pub port: u16,
pub name: Option<String>,
pub entry: Option<PathBuf>,
}
pub fn detect_project_info(dir: &Path) -> (String, PathBuf) {
let mut app_name = dir
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "amber-app".to_string());
let mut entry = PathBuf::from("app.ts");
let pkg_json_path = dir.join("package.json");
if pkg_json_path.exists() {
if let Ok(content) = fs::read_to_string(&pkg_json_path) {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(n) = val.get("name").and_then(|n| n.as_str()) {
app_name = n.to_string();
}
if let Some(m) = val.get("main").and_then(|m| m.as_str()) {
entry = PathBuf::from(m);
}
}
}
}
if !dir.join(&entry).exists() {
for candidate in &[
"app.ts",
"server.ts",
"index.ts",
"main.ts",
"app.js",
"server.js",
"index.js",
] {
let p = dir.join(candidate);
if p.exists() {
entry = PathBuf::from(candidate);
break;
}
}
}
(app_name, entry)
}
pub fn generate_docker_scaffold(
dir: &Path,
app_name: &str,
entry: &Path,
port: u16,
) -> Result<Vec<PathBuf>> {
let mut generated = Vec::new();
let dockerfile_path = dir.join("Dockerfile");
let dockerfile_content = format!(
r#"# Generated by Amber Deploy (https://github.com/zh30/amberjs)
# -------------------------------------------------------------
# Stage 1: Build & Bundle
# -------------------------------------------------------------
FROM alpine:3.20 AS builder
WORKDIR /build
# Copy project files
COPY . .
# Stage 2: Production Minimal Runtime
# -------------------------------------------------------------
FROM alpine:3.20 AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT={port}
EXPOSE {port}
COPY . .
# Run application using Amber web server mode
CMD ["amber", "serve", "{entry}", "--port", "{port}", "--host", "0.0.0.0"]
"#,
port = port,
entry = entry.display()
);
fs::write(&dockerfile_path, dockerfile_content)?;
generated.push(dockerfile_path);
let dockerignore_path = dir.join(".dockerignore");
let dockerignore_content = r#"node_modules
.git
.amberjs_cache
dist
coverage
target
*.log
.DS_Store
"#;
fs::write(&dockerignore_path, dockerignore_content)?;
generated.push(dockerignore_path);
let compose_path = dir.join("docker-compose.yml");
let compose_content = format!(
r#"version: '3.8'
services:
{app_name}:
build: .
ports:
- "{port}:{port}"
environment:
- NODE_ENV=production
- PORT={port}
restart: unless-stopped
"#,
app_name = app_name,
port = port
);
fs::write(&compose_path, compose_content)?;
generated.push(compose_path);
Ok(generated)
}
pub fn generate_k8s_scaffold(dir: &Path, app_name: &str, port: u16) -> Result<Vec<PathBuf>> {
let k8s_dir = dir.join("k8s");
fs::create_dir_all(&k8s_dir)?;
let mut generated = Vec::new();
let deploy_path = k8s_dir.join("deployment.yaml");
let deploy_content = format!(
r#"apiVersion: apps/v1
kind: Deployment
metadata:
name: {app_name}
labels:
app: {app_name}
spec:
replicas: 2
selector:
matchLabels:
app: {app_name}
template:
metadata:
labels:
app: {app_name}
spec:
containers:
- name: {app_name}
image: {app_name}:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: {port}
resources:
limits:
cpu: "1000m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
livenessProbe:
httpGet:
path: /
port: {port}
initialDelaySeconds: 3
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: {port}
initialDelaySeconds: 2
periodSeconds: 5
"#,
app_name = app_name,
port = port
);
fs::write(&deploy_path, deploy_content)?;
generated.push(deploy_path);
let svc_path = k8s_dir.join("service.yaml");
let svc_content = format!(
r#"apiVersion: v1
kind: Service
metadata:
name: {app_name}-service
spec:
type: ClusterIP
selector:
app: {app_name}
ports:
- port: 80
targetPort: {port}
"#,
app_name = app_name,
port = port
);
fs::write(&svc_path, svc_content)?;
generated.push(svc_path);
Ok(generated)
}
pub fn run_deploy(opts: DeployOptions) -> Result<()> {
let cwd = std::env::current_dir()?;
let (detected_name, detected_entry) = detect_project_info(&cwd);
let app_name = opts.name.unwrap_or(detected_name);
let entry = opts.entry.unwrap_or(detected_entry);
println!(
"🚀 Amber Deploy: Project '{}', Entry '{}'",
app_name,
entry.display()
);
match opts.target {
DeployTarget::Docker => {
let files = generate_docker_scaffold(&opts.output_dir, &app_name, &entry, opts.port)?;
println!("✅ Generated production Docker scaffolding:");
for f in files {
println!(" - {}", f.display());
}
println!("\nNext steps:");
println!(" docker compose up --build");
}
DeployTarget::Standalone => {
let dist_dir = opts.output_dir.join("dist");
fs::create_dir_all(&dist_dir)?;
let output_bin = dist_dir.join(&app_name);
crate::tooling::compiler::compile_binary(&entry, &output_bin)?;
println!("✅ Generated standalone binary executable:");
println!(" - {}", output_bin.display());
println!("\nNext steps:");
println!(" ./{}", output_bin.display());
}
DeployTarget::K8s => {
let files = generate_k8s_scaffold(&opts.output_dir, &app_name, opts.port)?;
println!("✅ Generated Kubernetes manifests in k8s/:");
for f in files {
println!(" - {}", f.display());
}
println!("\nNext steps:");
println!(" kubectl apply -f k8s/");
}
}
Ok(())
}