prctrl 1.0.0

Terminal-native GitHub PR management. Stay on top of code reviews without leaving your terminal.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::Result;
use chrono::Utc;

use crate::github::PendingReview;

/// Write a single review to a Markdown file.
/// Returns the path of the written file.
pub fn write_review(
    dir: &Path,
    review: &PendingReview,
    claude_summary: Option<&str>,
) -> Result<PathBuf> {
    fs::create_dir_all(dir)?;

    let slug = slugify(&review.pr_title);
    let filename = format!("{}-{}-{}.md", review.repo, review.pr_number, slug);
    let path = dir.join(&filename);

    let age_days = (Utc::now() - review.created_at).num_days();

    let mut content = format!(
        "# {title}\n\n\
         | Field       | Value |\n\
         |-------------|-------|\n\
         | **Repo**    | `{repo}` |\n\
         | **PR**      | [#{number}]({url}) |\n\
         | **Author**  | `{author}` |\n\
         | **Size**    | +{add} / -{del} lines |\n\
         | **Branch**  | `{branch}` |\n\
         | **Draft**   | {draft} |\n\
         | **Opened**  | {opened} ({age} days ago) |\n\n\
         ## 🔗 Link\n\n\
         {url}\n\n",
        title = review.pr_title,
        repo = review.repo,
        number = review.pr_number,
        url = review.pr_url,
        author = review.pr_author,
        add = review.additions,
        del = review.deletions,
        branch = review.branch,
        draft = if review.draft { "Yes" } else { "No" },
        opened = review.created_at.format("%Y-%m-%d"),
        age = age_days,
    );

    if let Some(summary) = claude_summary {
        content.push_str("## 🤖 Claude Triage\n\n");
        content.push_str(summary);
        content.push_str("\n\n---\n\n> Generated by prctrl\n");
    } else {
        content.push_str("## 📝 Notes\n\n_Add your review notes here._\n");
    }

    fs::write(&path, content)?;
    Ok(path)
}

/// Write an index of all pending reviews as INDEX.md
pub fn write_index(dir: &Path, reviews: &[PendingReview]) -> Result<PathBuf> {
    fs::create_dir_all(dir)?;
    let path = dir.join("INDEX.md");

    let mut content = format!(
        "# 📋 Pending Reviews — {}\n\n",
        Utc::now().format("%Y-%m-%d %H:%M UTC")
    );

    if reviews.is_empty() {
        content.push_str("✅ Nothing to review!\n");
    } else {
        content.push_str(&format!("{} PR(s) waiting for you.\n\n", reviews.len()));
        content.push_str("| # | PR | Repo | Author | Size | Age |\n");
        content.push_str("|---|----|----|--------|------|-----|\n");

        for r in reviews {
            let age = (Utc::now() - r.created_at).num_days();
            let slug = slugify(&r.pr_title);
            let file = format!("{}-{}-{}.md", r.repo, r.pr_number, slug);
            content.push_str(&format!(
                "| [#{}]({url}) | [{title}](./{file}) | `{repo}` | `{author}` | +{add}/-{del} | {age}d |\n",
                r.pr_number,
                url = r.pr_url,
                title = r.pr_title,
                repo = r.repo,
                author = r.pr_author,
                add = r.additions,
                del = r.deletions,
                age = age,
            ));
        }
    }

    fs::write(&path, content)?;
    Ok(path)
}

fn slugify(s: &str) -> String {
    s.to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '-' })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
        .chars()
        .take(40)
        .collect()
}