tix-tracker 0.4.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
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;

/// `tix init`: write the default `tix.toml` at `cwd`, seeding the planning
/// registry, then create the first `Backlog` milestone file. Refuses to
/// overwrite an existing `tix.toml` (per `contracts/tix-toml.md` §init).
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(())
}

/// Write the initial `tix.toml` with a documented empty `card_order = []`
/// entry so the user sees that only customized card orders are persisted.
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(())
}

/// Fill the default config's planning surface with the documented seed
/// (`contracts/tix-toml-planning.md`): the milestones dir, the auto-cycle
/// counter at `1`, a pre-created `Cycle 1`, the picker presets, and — when a
/// git identity can be read — a `default` team containing that identity
/// (FR-002).
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());
    }
}

/// Create the initial `Backlog` milestone file
/// (`contracts/milestone-file-format.md`), idempotently: a pre-existing
/// file is never overwritten (`create_milestone_file` refuses, and produces
/// an [`TixError::InvalidInput`], which we absorb).
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),
    }
}