use std::path::Path;
use crate::assignee::git_identity;
use crate::config::{Config, PlanningConfig, PlanningCycle, Team};
use crate::error::{Result, TixError};
use crate::planning::Planner;
pub fn run(cwd: &Path) -> Result<()> {
let path = cwd.join("tix.toml");
if path.exists() {
return Err(TixError::Config(format!(
"{} already exists — refusing to overwrite (delete it first if you want to re-init)",
path.display()
)));
}
let mut config = Config::starter();
seed_planning(&mut config);
write_init_config(&config, &path)?;
seed_milestone(cwd)?;
println!("Created {}", path.display());
Ok(())
}
fn write_init_config(config: &Config, path: &Path) -> Result<()> {
let dir = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => Path::new(".").to_path_buf(),
};
std::fs::create_dir_all(&dir)?;
let mut contents = toml::to_string_pretty(&config.clone())
.map_err(|e| TixError::Config(format!("cannot serialize tix.toml: {e}")))?;
contents = contents.replacen(
"order_by = \"created_at_desc\"",
"# Only cards whose ordering differs from the default are listed here.\ncard_order = []\norder_by = \"created_at_desc\"",
1,
);
contents.push_str(
"\n# Optional TUI overrides; all other actions keep their defaults.\n# See docs/key-bindings.md for the complete reference.\n# [keybindings.board]\n# new_issue = [\"ctrl+n\"]\n",
);
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("tix.toml");
let tmp = dir.join(format!("{file_name}.tmp-{}", std::process::id()));
let write_result = (|| -> std::io::Result<()> {
let mut f = std::fs::File::create(&tmp)?;
use std::io::Write;
f.write_all(contents.as_bytes())?;
f.sync_all()?;
Ok(())
})();
if let Err(e) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(TixError::Io(e));
}
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(TixError::Io(e));
}
Ok(())
}
fn seed_planning(config: &mut Config) {
config.milestones_dir = Some("docs/milestones".into());
config.planning = PlanningConfig {
default_cycle: Some("Cycle 1".into()),
default_milestone: Some("Backlog".into()),
default_team: None,
..PlanningConfig::default()
};
config
.cycles
.insert("Cycle 1".into(), PlanningCycle::default());
if let Some(identity) = git_identity() {
config.teams.insert(
"default".into(),
Team {
members: vec![identity.to_string()],
},
);
config.planning.default_team = Some("default".into());
}
}
fn seed_milestone(cwd: &Path) -> Result<()> {
let planner = Planner::from_config(Config::default(), cwd.to_path_buf());
match planner.create_milestone_file(
"Backlog",
None,
Some("Default milestone for unscheduled work"),
) {
Ok(_) => Ok(()),
Err(TixError::InvalidInput(_)) => Ok(()),
Err(e) => Err(e),
}
}