use crate::cli::WatchArgs;
use crate::dev::{run_dev_command, run_dev_watch};
use crate::service::{check_service, parse_simple_yaml};
use crate::util::{Result, make_vars, repo_root, run_cmd, run_make, usage_error};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) fn setup_current_service(args: &[String]) -> Result<()> {
let root = current_service_root()?;
apply_key_value_env(args);
println!("[setup] service: {}", root.display());
check_service(&root)?;
run_generators(&root)?;
install_language_dependencies(&root)?;
start_service_dependencies(&root)?;
println!("[setup] ready. Run `exe dev` from {}", root.display());
Ok(())
}
pub(crate) fn dev_current_service() -> Result<()> {
let root = current_service_root()?;
println!("[dev] service: {}", root.display());
run_generators(&root)?;
start_service_dependencies(&root)?;
let command = service_dev_command(&root)?;
run_dev_watch(WatchArgs {
env_file: service_env_file(&root).display().to_string(),
watch_root: root.display().to_string(),
ignore: vec![],
poll: 1.0,
command,
})
}
fn current_service_root() -> Result<PathBuf> {
let cwd = env::current_dir()?;
find_service_root_from(&cwd).ok_or_else(|| {
"service.yaml not found. Run this command from a service checkout or service subdirectory."
.into()
})
}
pub(crate) fn find_service_root_from(start: &Path) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
loop {
if dir.join("service.yaml").is_file() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
fn apply_key_value_env(args: &[String]) {
for item in make_vars(args) {
if let Some((key, value)) = item.split_once('=') {
unsafe { env::set_var(key, value) };
}
}
}
fn run_generators(root: &Path) -> Result<()> {
if truthy_env("SKIP_GENERATE") || truthy_env("EXE_SETUP_SKIP_GENERATE") {
println!("[setup] skipping generators");
return Ok(());
}
if root.join("Makefile").is_file() {
println!("[setup] running make generate");
return run_make(root, "generate", &[]);
}
let candidates = [
"tools/generate.sh",
"tools/generate-auth-artifacts.sh",
"tools/generate-gateway-artifacts.sh",
];
for candidate in candidates {
let path = root.join(candidate);
if path.is_file() {
println!("[setup] running {candidate}");
return run_cmd(root, "sh", &[candidate.to_string()]);
}
}
println!("[setup] no local generator found");
Ok(())
}
fn install_language_dependencies(root: &Path) -> Result<()> {
if truthy_env("SKIP_DEPS") || truthy_env("EXE_SETUP_SKIP_DEPS") {
println!("[setup] skipping language dependencies");
return Ok(());
}
if root.join("go.mod").is_file() {
println!("[setup] downloading Go modules");
run_cmd(root, "go", &["mod".into(), "download".into()])?;
}
if root.join("Cargo.toml").is_file() {
println!("[setup] fetching Rust crates");
run_cmd(root, "cargo", &["fetch".into()])?;
}
if root.join("package.json").is_file() {
if root.join("bun.lock").is_file() || root.join("bun.lockb").is_file() {
println!("[setup] installing Bun packages");
run_cmd(root, "bun", &["install".into()])?;
} else if root.join("package-lock.json").is_file() {
println!("[setup] installing npm packages");
run_cmd(root, "npm", &["ci".into()])?;
} else {
println!("[setup] package.json found; skipping install because no lockfile exists");
}
}
Ok(())
}
fn start_service_dependencies(root: &Path) -> Result<()> {
if truthy_env("SKIP_DOCKER") || truthy_env("EXE_SETUP_SKIP_DOCKER") {
println!("[setup] skipping Docker dependencies");
return Ok(());
}
if let Some(shared_compose) = shared_compose_file() {
println!("[setup] starting shared local dependencies");
unsafe { env::set_var("DEV_COMPOSE_FILE", shared_compose.display().to_string()) };
return run_dev_command(crate::cli::DevCommand::Up(crate::cli::KeyValueArgs {
vars: vec![],
}));
}
let compose = root.join("development/compose.dev.yml");
if !compose.is_file() {
println!("[setup] no development/compose.dev.yml found");
return Ok(());
}
println!("[setup] starting service dependencies");
run_cmd(
root,
"docker",
&[
"compose".into(),
"-f".into(),
compose.display().to_string(),
"up".into(),
"-d".into(),
],
)
}
fn shared_compose_file() -> Option<PathBuf> {
let path = repo_root().join("tools/local-dev/compose.yml");
path.is_file().then_some(path)
}
pub(crate) fn service_dev_command(root: &Path) -> Result<Vec<String>> {
let metadata = parse_simple_yaml(&root.join("service.yaml"));
let runtime = metadata.get("runtime").map(String::as_str).unwrap_or("");
let command = match runtime {
"go" if root.join("cmd/server/main.go").is_file() => vec!["go", "run", "./cmd/server"],
"go" => vec!["go", "run", "./cmd/server"],
"rust" => vec!["cargo", "run"],
"typescript" if has_script(root, "\"dev\"") => {
if root.join("bun.lock").is_file() || root.join("bun.lockb").is_file() {
vec!["bun", "run", "dev"]
} else {
vec!["npm", "run", "dev"]
}
}
"typescript" => vec!["node", "src/server.js"],
"python" => vec!["python", "-m", "src.server"],
_ => {
return usage_error(format!(
"unsupported service runtime `{runtime}` in {}",
root.join("service.yaml").display()
));
}
};
Ok(command.into_iter().map(String::from).collect())
}
pub(crate) fn service_env_file(root: &Path) -> PathBuf {
let local = root.join("configs/app.env");
if local.is_file() {
return local;
}
root.join("configs/app.env.example")
}
fn has_script(root: &Path, script_name: &str) -> bool {
fs::read_to_string(root.join("package.json"))
.map(|text| text.contains(script_name))
.unwrap_or(false)
}
fn truthy_env(name: &str) -> bool {
env::var(name)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}