release/0.3.0 #6

Merged
nvrl merged 5 commits from release/0.3.0 into main 2026-03-17 09:30:26 +01:00
3 changed files with 141 additions and 12 deletions
Showing only changes of commit 7b2217886c - Show all commits

33
Cargo.toml.out Normal file
View File

@@ -0,0 +1,33 @@
[[bin]]
name = "mould"
path = "src/main.rs"
[dependencies]
anyhow = "1.0.102"
crossterm = "0.29.0"
dirs = "6.0.0"
env_logger = "0.11.9"
log = "0.4.29"
ratatui = "0.30.0"
serde_json = "1.0.149"
serde_yaml = "0.9.34"
thiserror = "2.0.18"
toml = "1.0.6"
tui-input = "0.15.0"
[dependencies.clap]
features = ["derive"]
version = "4.6.0"
[dependencies.serde]
features = ["derive"]
version = "1.0.228"
[dev-dependencies]
tempfile = "3.27.0"
[package]
authors = ["Nils Pukropp <nils@narl.io>"]
edition = 2024
name = "mould"
version = "0.3.0"

View File

@@ -12,8 +12,8 @@ use std::path::PathBuf;
)] )]
pub struct Cli { pub struct Cli {
/// The input template file (e.g., .env.example, config.json.template, docker-compose.yml) /// The input template file (e.g., .env.example, config.json.template, docker-compose.yml)
#[arg(required = true, value_name = "INPUT_FILE")] #[arg(required = false, value_name = "INPUT_FILE")]
pub input: PathBuf, pub input: Option<PathBuf>,
/// Optional output file. If not provided, it will be inferred. /// Optional output file. If not provided, it will be inferred.
#[arg(short, long, value_name = "OUTPUT_FILE")] #[arg(short, long, value_name = "OUTPUT_FILE")]

View File

@@ -26,7 +26,8 @@ use ratatui::{Terminal, backend::CrosstermBackend};
fn determine_output_path(input: &Path) -> PathBuf { fn determine_output_path(input: &Path) -> PathBuf {
let file_name = input.file_name().unwrap_or_default().to_string_lossy(); let file_name = input.file_name().unwrap_or_default().to_string_lossy();
if file_name == ".env.example" { // Standard mappings
if file_name == ".env.example" || file_name == ".env.template" {
return input.with_file_name(".env"); return input.with_file_name(".env");
} }
@@ -37,11 +38,36 @@ fn determine_output_path(input: &Path) -> PathBuf {
return input.with_file_name("compose.override.yaml"); return input.with_file_name("compose.override.yaml");
} }
if file_name.ends_with(".example.json") { // Pattern-based mappings
return input.with_file_name(file_name.replace(".example.json", ".json")); if let Some(base) = file_name.strip_suffix(".env.example") {
return input.with_file_name(format!("{}.env", base));
} }
if file_name.ends_with(".template.json") { if let Some(base) = file_name.strip_suffix(".env.template") {
return input.with_file_name(file_name.replace(".template.json", ".json")); return input.with_file_name(format!("{}.env", base));
}
if let Some(base) = file_name.strip_suffix(".example.json") {
return input.with_file_name(format!("{}.json", base));
}
if let Some(base) = file_name.strip_suffix(".template.json") {
return input.with_file_name(format!("{}.json", base));
}
if let Some(base) = file_name.strip_suffix(".example.yml") {
return input.with_file_name(format!("{}.yml", base));
}
if let Some(base) = file_name.strip_suffix(".template.yml") {
return input.with_file_name(format!("{}.yml", base));
}
if let Some(base) = file_name.strip_suffix(".example.yaml") {
return input.with_file_name(format!("{}.yaml", base));
}
if let Some(base) = file_name.strip_suffix(".template.yaml") {
return input.with_file_name(format!("{}.yaml", base));
}
if let Some(base) = file_name.strip_suffix(".example.toml") {
return input.with_file_name(format!("{}.toml", base));
}
if let Some(base) = file_name.strip_suffix(".template.toml") {
return input.with_file_name(format!("{}.toml", base));
} }
input.with_extension(format!( input.with_extension(format!(
@@ -50,6 +76,60 @@ fn determine_output_path(input: &Path) -> PathBuf {
)) ))
} }
/// Discovers common configuration template files in the current directory.
fn find_input_file() -> Option<PathBuf> {
let candidates = [
".env.example",
"compose.yml",
"docker-compose.yml",
".env.template",
"compose.yaml",
"docker-compose.yaml",
];
// Priority 1: Exact matches for well-known defaults
for name in &candidates {
let path = PathBuf::from(name);
if path.exists() {
return Some(path);
}
}
// Priority 2: Pattern matches
if let Ok(entries) = std::fs::read_dir(".") {
let mut fallback = None;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.ends_with(".env.example")
|| name_str.ends_with(".env.template")
|| name_str.ends_with(".example.json")
|| name_str.ends_with(".template.json")
|| name_str.ends_with(".example.yml")
|| name_str.ends_with(".template.yml")
|| name_str.ends_with(".example.yaml")
|| name_str.ends_with(".template.yaml")
|| name_str.ends_with(".example.toml")
|| name_str.ends_with(".template.toml")
{
// Prefer .env.* or compose.* if multiple matches
if name_str.contains(".env") || name_str.contains("compose") {
return Some(entry.path());
}
if fallback.is_none() {
fallback = Some(entry.path());
}
}
}
if let Some(path) = fallback {
return Some(path);
}
}
None
}
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let args = cli::parse(); let args = cli::parse();
@@ -64,11 +144,27 @@ fn main() -> anyhow::Result<()> {
.format_timestamp(None) .format_timestamp(None)
.init(); .init();
let input_path = args.input; let input_path = match args.input {
if !input_path.exists() { Some(path) => {
error!("Input file not found: {}", input_path.display()); if !path.exists() {
return Err(MouldError::FileNotFound(input_path.display().to_string()).into()); error!("Input file not found: {}", path.display());
return Err(MouldError::FileNotFound(path.display().to_string()).into());
} }
path
}
None => match find_input_file() {
Some(path) => {
info!("Discovered template: {}", path.display());
path
}
None => {
error!("No template file provided and none discovered in current directory.");
println!("Usage: mould <INPUT_FILE>");
println!("Supported defaults: .env.example, compose.yml, docker-compose.yml, etc.");
return Err(MouldError::FileNotFound("None".to_string()).into());
}
},
};
info!("Input: {}", input_path.display()); info!("Input: {}", input_path.display());