use anyhow::{Context, Result};
use rand::Rng;
use std::fs;
use std::path::Path;
use std::process::Stdio;
use tokio::process::Command;
use crate::output;
const PASSWORD_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
pub async fn run(project_dir: &Path, generate_tls: bool, cors_origins: Option<&str>) -> Result<()> {
let validator_dir = find_validator_dir(project_dir)?;
output::info("Initializing KnishIO production deployment...");
let secrets_dir = validator_dir.join("secrets");
generate_secrets(&secrets_dir)?;
generate_config(&validator_dir)?;
generate_env(&validator_dir, cors_origins)?;
if generate_tls {
generate_tls_certs(&validator_dir).await?;
} else {
let certs_dir = validator_dir.join("certs");
if !certs_dir.join("server.pem").exists() {
output::warn("No TLS certificates found in certs/");
output::info(" Run: knishio init --tls to generate self-signed certs");
output::info(" Or place your own server.pem + server-key.pem in certs/");
}
}
fs::create_dir_all(validator_dir.join("backups"))
.context("Failed to create backups directory")?;
fs::create_dir_all(validator_dir.join("models"))
.context("Failed to create models directory")?;
println!();
output::success("Production setup complete!");
println!();
output::info("Next steps:");
output::info(" 1. Review .env.production — set CORS_ORIGINS to your domain");
output::info(" 2. knishio start --build -d");
output::info(" 3. knishio cell create YOUR_CELL_NAME");
output::info(" 4. knishio full (verify everything is healthy)");
println!();
output::info("Files created:");
output::info(&format!(" secrets/ — JWT secret + DB credentials"));
output::info(&format!(" knishio.toml — CLI config (points to production compose)"));
output::info(&format!(" .env.production — environment config"));
if generate_tls {
output::info(&format!(" certs/ — self-signed TLS certificates"));
}
Ok(())
}
fn generate_secrets(secrets_dir: &Path) -> Result<()> {
if secrets_dir.exists() {
output::warn("secrets/ already exists — skipping secret generation");
output::info(" Delete secrets/ and re-run to regenerate");
return Ok(());
}
fs::create_dir_all(secrets_dir).context("Failed to create secrets directory")?;
let jwt_secret = generate_hex(64);
fs::write(secrets_dir.join("jwt_secret"), &jwt_secret)
.context("Failed to write jwt_secret")?;
let db_password = generate_password(32);
fs::write(secrets_dir.join("db_password"), &db_password)
.context("Failed to write db_password")?;
let db_url = format!(
"postgres://knishio:{}@postgres:5432/knishio",
db_password
);
fs::write(secrets_dir.join("db_url"), &db_url)
.context("Failed to write db_url")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o600);
for name in &["jwt_secret", "db_password", "db_url"] {
let _ = fs::set_permissions(secrets_dir.join(name), perms.clone());
}
let dir_perms = fs::Permissions::from_mode(0o700);
let _ = fs::set_permissions(secrets_dir, dir_perms);
}
output::success("Generated secrets in secrets/");
Ok(())
}
fn generate_config(validator_dir: &Path) -> Result<()> {
let config_path = validator_dir.join("knishio.toml");
if config_path.exists() {
output::warn("knishio.toml already exists — skipping");
return Ok(());
}
let content = r#"# KnishIO CLI configuration — generated by `knishio init`
[validator]
url = "https://localhost:8080"
insecure_tls = true # Set to false once you have real TLS certificates
[docker]
compose_file = "docker-compose.production.yml"
postgres_container = "knishio-postgres"
validator_container = "knishio-validator"
[database]
user = "knishio"
name = "knishio"
"#;
fs::write(&config_path, content).context("Failed to write knishio.toml")?;
output::success("Generated knishio.toml");
Ok(())
}
fn generate_env(validator_dir: &Path, cors_origins: Option<&str>) -> Result<()> {
let env_path = validator_dir.join(".env.production");
if env_path.exists() {
output::warn(".env.production already exists — skipping");
return Ok(());
}
let origins = cors_origins.unwrap_or("https://your-app.example.com");
let content = format!(
r#"# KnishIO Validator — Production Environment
# Generated by `knishio init`
#
# Secrets are in ./secrets/ (injected via Docker _FILE convention).
# Do NOT put passwords in this file.
# Allowed CORS origins (comma-separated, no wildcard)
CORS_ORIGINS={origins}
# Host port for the validator
VALIDATOR_PORT=8080
# Embedding / Generation (disabled by default)
EMBEDDING_ENABLED=false
GENERATION_ENABLED=false
"#
);
fs::write(&env_path, content).context("Failed to write .env.production")?;
output::success("Generated .env.production");
Ok(())
}
async fn generate_tls_certs(validator_dir: &Path) -> Result<()> {
let certs_dir = validator_dir.join("certs");
if certs_dir.join("server.pem").exists() && certs_dir.join("server-key.pem").exists() {
output::warn("TLS certificates already exist in certs/ — skipping");
return Ok(());
}
fs::create_dir_all(&certs_dir).context("Failed to create certs directory")?;
output::info("Generating self-signed TLS certificate (valid 365 days)...");
let status = Command::new("openssl")
.args([
"req",
"-x509",
"-newkey",
"rsa:4096",
"-keyout",
])
.arg(certs_dir.join("server-key.pem"))
.args(["-out"])
.arg(certs_dir.join("server.pem"))
.args([
"-days",
"365",
"-nodes",
"-subj",
"/CN=knishio-validator",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.context("Failed to run openssl — is it installed?")?;
if status.success() {
output::success("Generated self-signed TLS certificate in certs/");
} else {
output::error("openssl certificate generation failed");
output::info(" Install openssl and re-run, or place certificates manually");
}
Ok(())
}
fn find_validator_dir(start: &Path) -> Result<std::path::PathBuf> {
if start.join("Dockerfile").exists() && start.join("migrations").is_dir() {
return Ok(start.to_path_buf());
}
let candidates = [
start.join("knishio-validator-rust"),
start.join("servers").join("knishio-validator-rust"),
];
for candidate in &candidates {
if candidate.join("Dockerfile").exists() {
return Ok(candidate.to_path_buf());
}
}
let mut dir = start.to_path_buf();
loop {
let candidate = dir.join("servers").join("knishio-validator-rust");
if candidate.join("Dockerfile").exists() {
return Ok(candidate);
}
if !dir.pop() {
break;
}
}
anyhow::bail!(
"Could not find the knishio-validator-rust directory.\n\
Run this command from inside the KnishIO project tree."
)
}
fn generate_hex(len: usize) -> String {
let mut rng = rand::thread_rng();
(0..len)
.map(|_| format!("{:x}", rng.gen::<u8>() % 16))
.collect()
}
fn generate_password(len: usize) -> String {
let mut rng = rand::thread_rng();
(0..len)
.map(|_| {
let idx = rng.gen_range(0..PASSWORD_CHARS.len());
PASSWORD_CHARS[idx] as char
})
.collect()
}