tbdflow 0.31.0

A CLI to streamline your Git workflow for Trunk-Based Development.
Documentation
use clap::{Parser, Subcommand};
use clap_complete::Shell;

#[derive(Parser, Debug)]
#[command(
    name = "tbdflow",
    author = "Claes Adamsson @cladam",
    version,
    about = "A CLI tool for Trunk-Based Development (TBD) workflows",
    long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
    /// Enable verbose output for debugging.
    #[arg(long)]
    pub verbose: bool,
    /// Simulate the command without making any changes.
    #[arg(long)]
    pub dry_run: bool,
    /// Emit machine-readable JSON output instead of human-readable text.
    #[arg(long, global = true)]
    pub json: bool,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Initialises the repository for Trunk-Based Development.
    #[command(after_help = "NON-INTERACTIVE USAGE:\n  \
    tbdflow init --yes                              # Use all defaults\n  \
    tbdflow init --yes --main-branch trunk          # Custom trunk name\n  \
    tbdflow init --yes --remote git@github.com:org/repo.git\n\n\
    FLAGS:\n  \
    --yes / -y          Accept defaults, skip all interactive prompts\n  \
    --main-branch       Set the main branch name (default: main)\n  \
    --remote            Link and push to a remote repository URL")]
    Init {
        /// Accept defaults and skip all interactive prompts (non-interactive mode).
        #[arg(
            short = 'y',
            long = "yes",
            alias = "defaults",
            alias = "non-interactive"
        )]
        non_interactive: bool,
        /// Set the main branch name (default: main).
        #[arg(long)]
        main_branch: Option<String>,
        /// Link a remote repository URL and push the initial commit.
        #[arg(long)]
        remote: Option<String>,
    },
    /// Shows the current tbdflow configuration.
    #[command(alias = "show")]
    Info {
        #[arg(short, long, default_value_t = false)]
        edit: bool,
    },
    /// Checks for a new version of tbdflow and updates it if available.
    Update,
    /// Commits changes to the current branch or 'main' if no branch is checked out.
    #[command(
        after_help = "Use the imperative, present tense: \"change\" not \"changed\". Think of This commit will...\n\
    COMMON COMMIT TYPES:\n  \
    feat:     A new feature for the user.\n  \
    fix:      A bug fix for the user.\n  \
    chore:    Routine tasks, maintenance, dependency updates.\n  \
    docs:     Documentation changes.\n  \
    style:    Code style changes (formatting, etc).\n  \
    refactor: Code changes that neither fix a bug nor add a feature.\n  \
    test:     Adding or improving tests.\n\n\
    EXAMPLES:\n  \
    tbdflow commit --type feat --scope api -m \"add user endpoint\"\n  \
    tbdflow commit -t fix -m \"fix login bug\" --breaking\n  \
    tbdflow commit -t chore -m \"update dependencies\" --tag \"v0.4.0\"\n  \
    tbdflow commit -t refactor -m \"rename internal API\" --breaking --breaking-description \"The `getUser` function has been renamed to `fetchUser`.\"\n  \
    tbdflow commit -t fix -s ui -m \"fix button alignment\" --issue \"#123\""
    )]
    Commit {
        /// Commit type (e.g. 'feat', 'fix', 'chore', 'docs').
        #[arg(short, long)]
        r#type: Option<String>,
        /// Optional scope of the commit.
        #[arg(short, long)]
        scope: Option<String>,
        /// The descriptive commit message.
        #[arg(short, long)]
        message: Option<String>,
        /// Mark this commit as a breaking change.
        #[arg(short, long)]
        breaking: bool,
        /// Optionally provide a description for the breaking change.
        #[arg(long)]
        breaking_description: Option<String>,
        /// Optionally add and push an annotated tag to this commit.
        #[arg(long)]
        tag: Option<String>,
        /// Optional flag to skip verification of the checklist.
        #[arg(long, default_value_t = false)]
        no_verify: bool,
        /// Optional flag for an issue reference.
        #[arg(long)]
        issue: Option<String>,
        /// Optional multi-line body for the commit message.
        #[arg(long)]
        body: Option<String>,
        /// Read the commit subject from a file ('-' for stdin). Avoids shell
        /// escaping. Conflicts with --message.
        #[arg(long, conflicts_with = "message")]
        message_file: Option<String>,
        /// Read the commit body from a file ('-' for stdin). Avoids multi-line
        /// shell escaping. Conflicts with --body.
        #[arg(long, conflicts_with = "body")]
        body_file: Option<String>,
        #[arg(long, default_value_t = false, hide = true)]
        /// Internal flag to do a global commit bypassing monorepo safety
        include_projects: bool,
    },
    /// Creates and pushes a new short-lived branch.
    #[command(after_help = "EXAMPLES:\n  \
    tbdflow branch --type feat --name \"user-profile-page\" --issue \"ABC-123\"\n  \
    tbdflow branch -t fix -n \"login-bug\" --issue \"CBA-456\n  \
    tbdflow branch -t chore -n \"update-dependencies\" -f \"39b68b5\"")]
    Branch {
        /// Type of branch (e.g., feat, fix, chore). See .tbdflow.yml for allowed types.
        #[arg(short, long)]
        r#type: Option<String>,
        /// A short, descriptive name for the branch.
        #[arg(short, long)]
        name: Option<String>,
        /// Optional issue reference to include in the branch name.
        #[arg(long)]
        issue: Option<String>,
        /// Optional commit hash on 'main' to branch from.
        #[arg(short, long)]
        from_commit: Option<String>,
    },
    /// Merges a short-lived branch into 'main' and deletes it.
    #[command(after_help = "EXAMPLES:\n  \
    tbdflow complete --type \"feature\" --name \"user-profile-page\"\n  \
    tbdflow complete -t \"release\" -n \"1.2.0\"")]
    Complete {
        /// Type of branch to complete, see .tbdflow.yml for allowed types.
        #[arg(short, long)]
        r#type: Option<String>,
        /// Name or version of the branch to complete.
        #[arg(short, long)]
        name: Option<String>,
    },
    /// Syncs with the remote, shows recent history, and checks for stale branches.
    /// When ci_check is enabled, checks trunk CI status before pulling.
    Sync,
    /// Scans active remote branches for overlapping work that may cause merge conflicts.
    #[command(
        name = "radar",
        after_help = "OVERLAP DETECTION FOR SOCIAL CODING:\n  \
    In TBD, everyone integrates frequently. Radar makes the invisible visible\n  \
    by showing who else is working on the same files — before you push.\n\n\
    DETECTION LEVELS (configurable in .tbdflow.yml):\n  \
    file:  Same files touched (fast, default)\n  \
    line:  Overlapping line ranges (precise)\n\n\
    EXAMPLES:\n  \
    tbdflow radar                          # Scan for overlaps\n  \
    tbdflow --verbose radar                # Scan with detailed git output\n  \
    tbdflow --dry-run radar                # Preview what would be checked"
    )]
    Radar,
    /// Shows the current git status.
    Status,
    /// Shows the current git branch name.
    #[command(name = "current-branch")]
    CurrentBranch,
    /// Checks for stale branches (older than 1 day).
    #[command(name = "check-branches")]
    CheckBranches,
    /// Generates a man page for the CLI.
    #[command(name = "generate-man-page", hide = true)] // Hidden from help
    #[command(after_help = "EXAMPLES:\n  \
    tbdflow generate-man-page > tbdflow.1\n \
    man ./tbdflow.1")]
    GenerateManPage,
    /// Generates shell completion scripts.
    #[command(name = "generate-completion", hide = true)] // Hidden from help
    Completion {
        #[arg(value_enum)]
        shell: Shell,
    },
    /// Generates a changelog from Conventional Commits.
    #[command(
        name = "changelog",
        after_help = "EXAMPLES:\n  \
    tbdflow changelog --from v1.0.0 --to v2.0.0\n  \
    tbdflow changelog --unreleased\n  \
    tbdflow changelog --from v1.0.0"
    )]
    Changelog {
        /// Generate from this git reference (tag or commit hash).
        #[arg(long)]
        from: Option<String>,
        /// Generate to this git reference (defaults to HEAD).
        #[arg(long)]
        to: Option<String>,
        /// Generate for all commits since the latest tag.
        #[arg(long, default_value_t = false)]
        unreleased: bool,
    },
    /// Internal commands for configuration.
    #[command(name = "config", hide = true)]
    Config {
        /// Print the DoD checklist items to stdout.
        #[arg(long)]
        get_dod: bool,
    },
    /// Prints the short SHA of the current HEAD commit.
    #[command(name = "head-sha", hide = true)]
    HeadSha,
    /// Reverts a commit on the trunk by its SHA. The panic button for trunk-based development.
    #[command(
        name = "undo",
        after_help = "THE PANIC BUTTON:\n  \
    In TBD, if the trunk breaks, fix it or revert it immediately.\n  \
    This command cleanly reverts a commit by SHA, syncs with remote first,\n  \
    and pushes the revert — restoring the trunk to a green state.\n\n\
    EXAMPLES:\n  \
    tbdflow undo abc1234                    # Revert a specific commit\n  \
    tbdflow undo abc1234 --no-push          # Revert locally without pushing\n  \
    tbdflow --dry-run undo abc1234          # Preview what would happen"
    )]
    Undo {
        /// The commit SHA to revert.
        sha: String,
        /// Skip pushing the revert commit to the remote.
        #[arg(long, default_value_t = false)]
        no_push: bool,
    },
    /// Logs an intent note (breadcrumb) during development.
    /// Notes are captured in a local .tbdflow-intent.json and included
    /// in the next commit message as an Intent Log.
    #[command(
        name = "note",
        aliases = ["n", "+"],
        after_help = "INTENT LOG — CAPTURE THE \"WHY\" BEFORE IT'S LOST:\n  \
    Use this command to leave breadcrumbs while you work.\n  \
    Notes are appended to .tbdflow-intent.json (never committed)\n  \
    and automatically included in your next commit message.\n\n\
    SHORTCUTS:\n  \
    tbdflow n     Short alias\n  \
    tbdflow +     Append alias\n\n\
    EXAMPLES:\n  \
    tbdflow + \"trying factory pattern\"\n  \
    tbdflow n \"factory too complex, switching to trait\"\n  \
    tbdflow note --show"
    )]
    Note {
        /// The note message to record.
        message: Option<String>,
        /// Show the current intent log instead of adding a note.
        #[arg(long, default_value_t = false)]
        show: bool,
    },
    /// Manages the current task context for the intent log.
    #[command(
        name = "task",
        subcommand,
        after_help = "TASK CONTEXT — NAME YOUR WORK:\n  \
    Start a task to give your intent notes a heading.\n  \
    The task name appears in the Intent Log section of your commit.\n\n\
    EXAMPLES:\n  \
    tbdflow task start \"Refactor auth logic\"\n  \
    tbdflow task show\n  \
    tbdflow task clear"
    )]
    Task(TaskAction),
    /// Recovers a WIP snapshot from the safety log.
    /// Snapshots are captured automatically during notes and syncs.
    #[command(
        name = "recover",
        after_help = "tbdflow WIP Guard\n  \
    Snapshots are captured automatically when you use 'tbdflow n', 'tbdflow sync' or 'tbdflow radar'.\n  \
    Use this command to list and restore them.\n\n\
    EXAMPLES:\n  \
    tbdflow recover --list                # Show available snapshots\n  \
    tbdflow recover 1                     # Restore snapshot #1\n  \
    tbdflow recover a7b8c9d0              # Restore by hash"
    )]
    Recover {
        /// Snapshot index or hash to restore.
        selector: Option<String>,
        /// List all available snapshots instead of restoring.
        #[arg(long, default_value_t = false)]
        list: bool,
    },
    /// Manages non-blocking post-commit reviews for trunk-based development.
    #[command(
        name = "review",
        after_help = "EXAMPLES:\n  \
        tbdflow review abc1234                      # Create review for a specific commit\n  \
        tbdflow review --trigger                    # Create review for HEAD commit\n  \
        tbdflow review --digest                     # Show commits since yesterday\n  \
        tbdflow review --digest --since \"3 days ago\"\n  \
        tbdflow review --approve abc1234           # Mark commit as reviewed\n  \
        tbdflow review --concern abc1234 -m \"Thread safety issue\"\n  \
        tbdflow review --dismiss abc1234 -m \"Won't fix, out of scope\"\n\n\
        WORKFLOW:\n  \
        1. Commit directly to main with 'tbdflow commit'\n  \
        2. Review is triggered automatically (if enabled) or manually\n  \
        3. Team reviews asynchronously without blocking\n  \
        4. Use --concern to flag issues (keeps issue open)\n  \
        5. Use --approve to mark commits as reviewed\n  \
        6. Use --dismiss to close without fixing"
    )]
    Review {
        /// Commit SHA to trigger a review for. If given without flags, triggers a review.
        #[arg(conflicts_with_all = ["digest", "approve", "concern", "dismiss"])]
        sha: Option<String>,
        /// Trigger a review request for the current HEAD commit.
        #[arg(long, conflicts_with_all = ["digest", "approve", "concern", "dismiss"])]
        trigger: bool,
        /// Generate a digest of commits needing review.
        #[arg(long, conflicts_with_all = ["trigger", "approve", "concern", "dismiss"])]
        digest: bool,
        /// Mark a specific commit as approved/reviewed (closes issue with review-accepted label).
        #[arg(long, conflicts_with_all = ["trigger", "digest", "concern", "dismiss"])]
        approve: Option<String>,
        /// Raise a concern on a commit (keeps issue open, adds review-concern label).
        #[arg(long, conflicts_with_all = ["trigger", "digest", "approve", "dismiss"])]
        concern: Option<String>,
        /// Dismiss a review (closes issue with review-dismissed label).
        #[arg(long, conflicts_with_all = ["trigger", "digest", "approve", "concern"])]
        dismiss: Option<String>,
        /// Message for concern or dismiss (required with --concern or --dismiss).
        #[arg(short, long)]
        message: Option<String>,
        /// Time range for digest (e.g., "1 day ago", "2024-01-01").
        #[arg(long, default_value = "1 day ago")]
        since: String,
        /// Override default reviewers (comma-separated GitHub usernames).
        #[arg(long, value_delimiter = ',')]
        reviewers: Option<Vec<String>>,
    },
}

/// Sub-actions for the `tbdflow task` command.
#[derive(Subcommand, Debug)]
pub enum TaskAction {
    /// Start a new task context for the intent log.
    Start {
        /// A short description of the task (e.g. "Refactor auth logic").
        description: String,
    },
    /// Show the current task and intent log.
    Show,
    /// Clear the current intent log (removes .tbdflow-intent.json).
    Clear,
}