use anyhow::Result;
use std::path::PathBuf;
use crate::error_code::{self, ErrorCodeExt};
use crate::telemetry;
use super::Config;
use super::format::{CONFIG_FORMATS, ConfigFileFormat, format_handler};
use super::loader_js::{JS_CONFIG_FILENAME, TS_CONFIG_FILENAME};
use super::package::{FileFormat, PackageConfig, VersionedFile};
use super::workspace::WorkspaceConfig;
fn prompt(question: &str, default: &str) -> String {
use std::io::Write;
if default.is_empty() {
print!("{question}: ");
} else {
print!("{question} [{default}]: ");
}
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
let trimmed = input.trim().to_string();
if trimmed.is_empty() {
default.to_string()
} else {
trimmed
}
}
fn prompt_bool(question: &str, default: bool) -> bool {
let hint = if default { "Y/n" } else { "y/N" };
let answer = prompt(&format!("{question} [{hint}]"), "");
match answer.to_lowercase().as_str() {
"y" | "yes" => true,
"n" | "no" => false,
_ => default,
}
}
const ALLOWED_FORMATS: &[&str] = &["toml", "json", "xml", "gradle", "gomod", "txt"];
fn prompt_format(indent: bool) -> String {
let question = if indent {
" Version file format [toml/json/xml/gradle/gomod/txt]"
} else {
"Version file format [toml/json/xml/gradle/gomod/txt]"
};
loop {
let input = prompt(question, "toml");
let normalized = input.trim().to_lowercase();
if ALLOWED_FORMATS.contains(&normalized.as_str()) {
return normalized;
}
eprintln!(
"Invalid format '{}'. Allowed values: toml, json, xml, gradle, gomod, txt.",
input
);
}
}
const ALLOWED_CONFIG_FORMATS: &[&str] = &["json", "json5", "toml", "dotfile"];
fn prompt_config_format() -> ConfigFileFormat {
let question = "Config file format [json/json5/toml/dotfile]";
loop {
let input = prompt(question, "json");
let normalized = input.trim().to_lowercase();
if ALLOWED_CONFIG_FORMATS.contains(&normalized.as_str()) {
return match normalized.as_str() {
"json5" => ConfigFileFormat::Json5,
"toml" => ConfigFileFormat::Toml,
"dotfile" | ".ferrflow" => ConfigFileFormat::Dotfile,
_ => ConfigFileFormat::Json,
};
}
eprintln!(
"Invalid format '{}'. Allowed values: json, json5, toml, dotfile.",
input
);
}
}
fn default_version_file(format: &str) -> &'static str {
match format {
"json" => "package.json",
"xml" => "pom.xml",
"gradle" => "build.gradle",
"gomod" => "go.mod",
"txt" => "VERSION.txt",
_ => "Cargo.toml",
}
}
fn parse_file_format(s: &str) -> FileFormat {
match s {
"json" => FileFormat::Json,
"xml" => FileFormat::Xml,
"gradle" => FileFormat::Gradle,
"gomod" => FileFormat::GoMod,
"txt" => FileFormat::Txt,
_ => FileFormat::Toml,
}
}
fn collect_package(path_default: &str, monorepo: bool) -> PackageConfig {
let dir_name = std::env::current_dir()
.ok()
.and_then(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
})
.unwrap_or_else(|| "project".to_string());
let name = if monorepo {
prompt(" Package name", "")
} else {
prompt("Package name", &dir_name)
};
let path = prompt(if monorepo { " Path" } else { "Path" }, path_default);
let format_str = prompt_format(monorepo);
let version_file_default = default_version_file(&format_str);
let version_file_path = if path == "." {
prompt(
if monorepo {
" Version file path"
} else {
"Version file path"
},
version_file_default,
)
} else {
prompt(
if monorepo {
" Version file path"
} else {
"Version file path"
},
&format!("{path}/{version_file_default}"),
)
};
let changelog_default = if path == "." {
"CHANGELOG.md".to_string()
} else {
format!("{path}/CHANGELOG.md")
};
let changelog = prompt(
if monorepo {
" Changelog path"
} else {
"Changelog path"
},
&changelog_default,
);
PackageConfig {
name,
path,
versioned_files: vec![VersionedFile {
path: version_file_path,
format: parse_file_format(&format_str),
selector: None,
}],
changelog: Some(changelog),
shared_paths: Vec::new(),
depends_on: vec![],
versioning: None,
tag_template: None,
hooks: None,
floating_tags: None,
}
}
pub fn init(format: Option<ConfigFileFormat>) -> Result<()> {
for handler in CONFIG_FORMATS {
let path = PathBuf::from(handler.filename());
if path.exists() {
Err(anyhow::anyhow!("{} already exists", handler.filename()))
.error_code(error_code::CONFIG_ALREADY_EXISTS)?;
}
}
for filename in [TS_CONFIG_FILENAME, JS_CONFIG_FILENAME] {
let path = PathBuf::from(filename);
if path.exists() {
Err(anyhow::anyhow!("{filename} already exists"))
.error_code(error_code::CONFIG_ALREADY_EXISTS)?;
}
}
let fmt = format.unwrap_or_else(prompt_config_format);
let handler = format_handler(fmt);
let monorepo = prompt_bool("Is this a monorepo?", false);
let packages = if monorepo {
println!("Add packages (leave name empty to finish):");
let mut pkgs = Vec::new();
loop {
let pkg = collect_package("", true);
if pkg.name.is_empty() {
if pkgs.is_empty() {
eprintln!("At least one package is required.");
continue;
}
break;
}
pkgs.push(pkg);
}
pkgs
} else {
vec![collect_package(".", false)]
};
let config = Config {
workspace: WorkspaceConfig::default(),
packages,
};
let content = handler.serialize(&config)?;
let filename = handler.filename();
std::fs::write(filename, &content)?;
println!("Created {filename}");
println!("Run: ferrflow check");
if config.workspace.anonymous_telemetry {
telemetry::send_event(telemetry::EventType::Init, None, None, None, None);
}
Ok(())
}