use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use crate::cli::NewArgs;
use crate::commands::make_app;
use crate::{generator, naming, templates, timestamp};
pub fn run(args: &NewArgs) -> Result<()> {
let name = args.name.as_str();
naming::validate_project_name(name)?;
let framework_dependency = framework_dependency(args.framework_path.as_deref())?;
let gateway_dependency = gateway_dependency(&framework_dependency)?;
let target = std::env::current_dir()
.context("failed to read the current directory")?
.join(name);
if target.exists() {
bail!("destination `{}` already exists", target.display());
}
fs::create_dir(&target)
.with_context(|| format!("failed to create directory {}", target.display()))?;
let created =
scaffold(&target, name, &framework_dependency, &gateway_dependency).and_then(|()| {
let admin = make_app::Spec {
name: "admin",
host: Some("admin.localhost"),
path: None,
port: None,
separate: false,
customer: false,
kind: make_app::Kind::Admin,
};
make_app::create(&target, &admin).map(|_| ())
});
if let Err(err) = created {
let _ = fs::remove_dir_all(&target);
return Err(err);
}
println!("\nApplication `{name}` created successfully.\n");
println!("AI coding agents: see {name}/AGENTS.md (CLAUDE.md imports it).\n");
println!("Next steps:");
println!(" cd {name}");
println!(
" surreal start --user root --pass root # or set SURREAL_* in .env to a hosted SurrealDB"
);
println!(" slave migrate # create the tables");
println!(" slave build # the first build takes a few minutes, later ones seconds");
println!(" slave serve # sign-in codes are printed in the log until SMTP_URL is set");
println!("\nThen open http://admin.localhost:8000; the first user becomes super admin.");
println!("`slave doctor` checks the setup; set SMTP_URL and MAIL_FROM in .env to email codes.");
println!("\nWeb apps: `slave make:app <name> [--domain <host>] [--path </prefix>]`, served by");
println!("the runtime (runtime/); add --separate to run one as a process of its own.");
Ok(())
}
fn scaffold(target: &Path, name: &str, framework: &str, gateway: &str) -> Result<()> {
let migration_ts = timestamp::migration_prefix_now()?;
let crate_name = name.replace('-', "_");
let vars = [
("name", name),
("crate_name", crate_name.as_str()),
("slave_version", env!("CARGO_PKG_VERSION")),
("framework_dependency", framework),
("gateway_dependency", gateway),
("migration_ts", migration_ts.as_str()),
];
for file in templates::PROJECT {
let path = templates::render(file.path, &vars);
let contents = templates::render(file.contents, &vars);
generator::write_file(&target.join(&path), &contents, false)?;
println!(" created {name}/{path}");
}
let env_example = templates::PROJECT
.iter()
.find(|file| file.path == ".env.example")
.map(|file| templates::render(file.contents, &vars))
.unwrap_or_default();
generator::write_file(&target.join(".env"), &env_example, false)?;
println!(" created {name}/.env");
Ok(())
}
fn framework_dependency(local_path: Option<&Path>) -> Result<String> {
match local_path {
None => Ok(format!(
"slave-framework = {{ version = \"{}\", default-features = false }}",
templates::FRAMEWORK_VERSION
)),
Some(path) => {
let path = local_crate(path, "slave-framework")?;
Ok(format!(
"slave-framework = {{ path = \"{path}\", default-features = false }}"
))
}
}
}
pub(crate) fn local_crate(path: &Path, name: &str) -> Result<String> {
let path = path
.canonicalize()
.with_context(|| format!("path {} does not exist", path.display()))?;
let manifest = fs::read_to_string(path.join("Cargo.toml")).unwrap_or_default();
if !manifest.contains(&format!("name = \"{name}\"")) {
bail!("{} does not contain the {name} crate", path.display());
}
Ok(path
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\""))
}
fn gateway_dependency(framework_line: &str) -> Result<String> {
let local = framework_line
.split_once("path = \"")
.and_then(|(_, rest)| rest.split_once('"'))
.map(|(path, _)| Path::new(path).with_file_name("gateway"))
.filter(|path| path.join("Cargo.toml").is_file());
match local {
Some(path) => {
let path = local_crate(&path, "slave-gateway")?;
Ok(format!(
"slave-gateway = {{ path = \"{path}\", default-features = false }}"
))
}
None => Ok(format!(
"slave-gateway = {{ version = \"{}\", default-features = false }}",
templates::GATEWAY_VERSION
)),
}
}