use std::{fs, path::Path, process::Command};
use console::style;
pub fn dev() -> anyhow::Result<()> {
println!(
"{} Starting dev server (cargo watch)...",
style("rok dev").green().bold()
);
println!(" Watching src/ — restart on change");
println!(" Ctrl+C to stop\n");
let status = Command::new("cargo")
.args(["watch", "--watch", "src/", "-x", "run"])
.status();
match status {
Ok(s) if s.success() => Ok(()),
Ok(_) => anyhow::bail!("cargo watch exited with error"),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
anyhow::bail!("cargo-watch not found. Install it with:\n cargo install cargo-watch")
}
Err(e) => Err(e.into()),
}
}
pub fn serve(port: Option<u16>, release: bool) -> anyhow::Result<()> {
let _ = dotenvy::dotenv();
let mut env: Vec<(String, String)> = Vec::new();
if let Some(p) = port {
env.push(("LISTEN_ADDR".to_string(), format!("0.0.0.0:{p}")));
println!(
"{} Listening on port {p}",
style("rok serve").green().bold()
);
}
let cargo_cmd = if release { "run --release" } else { "run" };
let watch_test = Command::new("cargo").args(["watch", "--version"]).output();
let use_watch = watch_test.map(|o| o.status.success()).unwrap_or(false);
if use_watch {
println!(
"{} Starting with cargo-watch (auto-reload on change)...",
style("rok serve").green().bold()
);
println!(" Ctrl+C to stop\n");
let mut cmd = Command::new("cargo");
cmd.args(["watch", "--watch", "src/", "-x", cargo_cmd]);
for (k, v) in &env {
cmd.env(k, v);
}
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("cargo watch exited with error");
}
} else {
println!(
"{} Starting server (install cargo-watch for auto-reload)...",
style("rok serve").green().bold()
);
println!(" Tip: cargo install cargo-watch\n");
let mut args = vec!["run"];
if release {
args.push("--release");
}
let mut cmd = Command::new("cargo");
cmd.args(&args);
for (k, v) in &env {
cmd.env(k, v);
}
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("cargo run exited with error");
}
}
Ok(())
}
pub fn env_check() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
println!(
"{} Scanning config files for required env vars...\n",
style("env:check").green().bold()
);
let config_dir = Path::new("src/config");
if !config_dir.exists() {
anyhow::bail!("src/config/ not found — is this a rok project?");
}
let mut keys: Vec<String> = Vec::new();
for entry in fs::read_dir(config_dir)? {
let entry = entry?;
if entry.path().extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
if entry
.file_name()
.to_str()
.map(|n| n == "mod.rs")
.unwrap_or(false)
{
continue;
}
let source = fs::read_to_string(entry.path())?;
collect_env_keys(&source, &mut keys);
}
if keys.is_empty() {
println!(" No #[env(...)] attributes found in src/config/");
return Ok(());
}
keys.sort();
keys.dedup();
let mut pass = 0usize;
let mut fail = 0usize;
for key in &keys {
match std::env::var(key) {
Ok(val) if !val.is_empty() => {
let preview = if val.len() > 20 {
format!("{}…", &val[..20])
} else {
val.clone()
};
println!(
" {} {:<30} = {}",
style("✓").green(),
key,
style(preview).dim()
);
pass += 1;
}
_ => {
println!(" {} {}", style("✗").red(), style(key).red().bold());
fail += 1;
}
}
}
println!();
if fail == 0 {
println!("{} All {} env vars set.", style("✓").green().bold(), pass);
} else {
println!(
"{} {pass} set, {} missing — add them to your .env file.",
style("!").yellow().bold(),
style(fail).red().bold()
);
}
Ok(())
}
fn collect_env_keys(source: &str, keys: &mut Vec<String>) {
let chars = source.char_indices().peekable();
let marker = "#[env(\"";
for (i, _) in chars {
if source[i..].starts_with(marker) {
let start = i + marker.len();
if let Some(end) = source[start..].find('"') {
let key = &source[start..start + end];
if !key.is_empty() {
keys.push(key.to_string());
}
}
}
}
}
#[allow(dead_code)]
#[derive(Debug, Default)]
pub struct RokConfig {
pub default_id: Option<String>,
pub default_auth: bool,
pub soft_delete: bool,
pub with_tests: bool,
pub with_factory: bool,
}
#[allow(dead_code)]
impl RokConfig {
pub fn load() -> Self {
let path = Path::new(".rok/config.toml");
if !path.exists() {
return Self::default();
}
let Ok(content) = fs::read_to_string(path) else {
return Self::default();
};
let mut cfg = RokConfig::default();
for line in content.lines() {
let line = line.trim();
if let Some(val) = line.strip_prefix("default_id") {
cfg.default_id = extract_toml_string(val);
} else if line.starts_with("default_auth") {
cfg.default_auth = extract_toml_bool(line);
} else if line.starts_with("soft_delete") {
cfg.soft_delete = extract_toml_bool(line);
} else if line.starts_with("with_tests") {
cfg.with_tests = extract_toml_bool(line);
} else if line.starts_with("with_factory") {
cfg.with_factory = extract_toml_bool(line);
}
}
cfg
}
}
#[allow(dead_code)]
fn extract_toml_string(rest: &str) -> Option<String> {
let rest = rest.trim().trim_start_matches('=').trim();
if rest.starts_with('"') && rest.ends_with('"') {
Some(rest[1..rest.len() - 1].to_string())
} else {
None
}
}
#[allow(dead_code)]
fn extract_toml_bool(line: &str) -> bool {
line.contains("true")
}
pub fn config_show(as_json: bool) -> anyhow::Result<()> {
use console::style;
dotenvy::dotenv().ok();
let env_path = std::path::Path::new(".env");
let pairs: Vec<(String, String)> = if env_path.exists() {
let content = fs::read_to_string(env_path)?;
content
.lines()
.filter(|l| !l.trim_start().starts_with('#') && l.contains('='))
.map(|l| {
let mut parts = l.splitn(2, '=');
let key = parts.next().unwrap_or("").trim().to_string();
let raw = parts.next().unwrap_or("").to_string();
let val = if is_secret(&key) {
mask_value(&raw)
} else {
raw
};
(key, val)
})
.filter(|(k, _)| !k.is_empty())
.collect()
} else {
Vec::new()
};
if pairs.is_empty() {
if as_json {
println!("{{}}");
} else {
println!("{}", style("No .env file found.").yellow());
println!("Create a .env file in the project root to configure your application.");
}
return Ok(());
}
if as_json {
let map: serde_json::Map<String, serde_json::Value> = pairs
.into_iter()
.map(|(k, v)| (k, serde_json::Value::String(v)))
.collect();
println!("{}", serde_json::to_string_pretty(&map)?);
} else {
println!("{}", style("Environment Configuration").green().bold());
println!();
for (key, val) in &pairs {
println!(" {:<35} {}", style(key).cyan(), val);
}
println!();
println!(
" {} {} variable(s) • secrets masked",
style("✓").green(),
pairs.len()
);
}
Ok(())
}
const SECRET_SUBSTRINGS: &[&str] = &[
"PASSWORD",
"SECRET",
"KEY",
"TOKEN",
"PASS",
"AUTH",
"HASH",
"CREDENTIAL",
"CERT",
"PRIVATE",
];
fn is_secret(key: &str) -> bool {
let upper = key.to_uppercase();
SECRET_SUBSTRINGS.iter().any(|s| upper.contains(s))
}
fn mask_value(val: &str) -> String {
if val.is_empty() {
return String::new();
}
if val.len() <= 4 {
return "****".to_string();
}
format!("{}…{}", &val[..2], &val[val.len() - 2..])
}
pub fn init_rok_config() -> anyhow::Result<()> {
let dir = Path::new(".rok");
let path = dir.join("config.toml");
if path.exists() {
println!(
"{} .rok/config.toml already exists.",
style("skip").yellow()
);
return Ok(());
}
fs::create_dir_all(dir)?;
fs::write(
&path,
r#"# rok project configuration
# These values are used as defaults by code generators.
# default_id = "ulid" # ulid | cuid2 | uuid_v7 | snowflake | nanoid
default_auth = false
soft_delete = false
with_tests = true
with_factory = true
"#,
)?;
println!("{} Created .rok/config.toml", style("✓").green().bold());
Ok(())
}