use crate::config::{RailConfig, UnifyConfig};
use crate::error::{RailError, RailResult};
use crate::progress;
use crate::toml::RailConfigBuilder;
use crate::workspace::WorkspaceContext;
use std::fs;
use std::path::{Path, PathBuf};
pub fn run_init(ctx: &WorkspaceContext, output_path: &str, force: bool, check: bool, json: bool) -> RailResult<()> {
run_init_impl(ctx.workspace_root(), output_path, force, check, json)
}
fn default_unify_config() -> UnifyConfig {
UnifyConfig::default()
}
fn detect_targets(workspace_root: &Path) -> Vec<String> {
crate::targets::detect_targets(workspace_root).unwrap_or_default()
}
fn build_rail_config(targets: Vec<String>) -> RailConfig {
RailConfig {
targets,
unify: default_unify_config(),
release: crate::config::ReleaseConfig::default(),
change_detection: crate::config::ChangeDetectionConfig::default(),
run: crate::config::RunConfig::default(),
crates: Default::default(),
}
}
fn check_existing_config(workspace_root: &Path) -> Option<PathBuf> {
crate::config::RailConfig::find_config_path(workspace_root)
}
fn ensure_output_dir(output_path: &Path) -> RailResult<()> {
if let Some(parent) = output_path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)
.map_err(|e| RailError::message(format!("failed to create {}: {}", parent.display(), e)))?;
}
Ok(())
}
fn write_config_file(config_path: &Path, content: &str) -> RailResult<()> {
let temp_path = config_path.with_extension("toml.tmp");
fs::write(&temp_path, content).map_err(|e| {
RailError::with_help(
format!("failed to write {}: {}", temp_path.display(), e),
"check file permissions",
)
})?;
fs::rename(&temp_path, config_path).map_err(|e| RailError::message(format!("failed to finalize config: {}", e)))?;
Ok(())
}
fn is_cargo_workspace(workspace_root: &Path) -> bool {
let cargo_toml = workspace_root.join("Cargo.toml");
if !cargo_toml.exists() {
return false;
}
if let Ok(content) = fs::read_to_string(&cargo_toml) {
content.contains("[workspace]")
} else {
false
}
}
fn run_init_impl(workspace_root: &Path, output_path: &str, force: bool, check: bool, json: bool) -> RailResult<()> {
let config_path = workspace_root.join(output_path);
if !is_cargo_workspace(workspace_root) && !check && !json && !crate::output::is_quiet() {
crate::warn!("no Cargo workspace detected in {}", workspace_root.display());
eprintln!(" cargo-rail works best with Cargo workspaces\n");
}
if config_path.exists() && !check {
if !force {
return Err(RailError::with_help(
format!("configuration exists: {}", config_path.display()),
"use --force to overwrite, or use --check to preview what would be generated",
));
}
if !json {
progress!("overwriting: {}", config_path.display());
}
} else if let Some(existing) = check_existing_config(workspace_root) {
if !check && !json && !crate::output::is_quiet() {
crate::note!("existing config found at {}", existing.display());
eprintln!(" (search order: rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml)\n");
}
}
let detected_targets = detect_targets(workspace_root);
let config = build_rail_config(detected_targets.clone());
let toml_content = RailConfigBuilder::new()
.header()
.targets(&config.targets)
.unify(&config.unify)
.release(&config.release)
.change_detection(&config.change_detection)
.run(&config.run)
.splits_template()
.build()?;
if check {
if json {
let payload = serde_json::json!({
"command": "init",
"check": true,
"config_path": config_path.display().to_string(),
"targets_detected": detected_targets,
"content": toml_content
});
let output = crate::output::machine_json_envelope("init", "check", "success", 0, payload);
println!("{}", serde_json::to_string_pretty(&output).unwrap_or_default());
} else {
println!("{}", toml_content);
}
} else {
ensure_output_dir(&config_path)?;
write_config_file(&config_path, &toml_content)?;
if json {
let payload = serde_json::json!({
"command": "init",
"status": "created",
"config_path": config_path.display().to_string(),
"targets_detected": detected_targets
});
let output = crate::output::machine_json_envelope("init", "apply", "created", 0, payload);
println!("{}", serde_json::to_string_pretty(&output).unwrap_or_default());
} else {
progress!("created: {}", config_path.display());
progress!("\nnext: cargo rail unify --check");
}
}
Ok(())
}
pub fn run_init_standalone(
workspace_root: &Path,
output_path: &str,
force: bool,
check: bool,
json: bool,
) -> RailResult<()> {
run_init_impl(workspace_root, output_path, force, check, json)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ReleaseConfig;
#[test]
fn test_serialize_config_with_builder() {
let config = RailConfig {
targets: vec![],
unify: UnifyConfig::default(),
release: ReleaseConfig::default(),
change_detection: crate::config::ChangeDetectionConfig::default(),
run: crate::config::RunConfig::default(),
crates: Default::default(),
};
let toml = RailConfigBuilder::new()
.header()
.targets(&config.targets)
.unify(&config.unify)
.release(&config.release)
.change_detection(&config.change_detection)
.run(&config.run)
.splits_template()
.build()
.unwrap();
assert!(toml.contains("[unify]"));
assert!(toml.contains("[change-detection]"));
assert!(toml.contains("cargo-rail configuration"));
assert!(toml.contains("Documentation:"));
}
}