loopflow 0.10.1

Run steps and flows with coding agents
Documentation
use std::path::Path;

use clap::Parser;

use crate::engine::flow::Op;
use crate::engine::git::{get_default_branch, sync_main};
use crate::engine::{sync_skills, SkillSyncOptions};
use crate::lf::{Cli, Commands, OpsCommand, ReleaseCommand};
use crate::ops::error::{OpsError, OpsResult};
use crate::ops::progress::Progress;
use crate::ops::{
    abandon_branch, commit_workflow, create_or_update_pr, land, next_branch, rebase_with_recovery,
    release_bump, release_check, release_notes, release_run, release_status, release_tag, submit,
    AbandonOptions, CommitOptions, LandOptions, NextOptions, PrOptions, RebaseOptions,
};

pub fn execute_flow_ops(repo: &Path, item: &Op, progress: &impl Progress) -> OpsResult<()> {
    let mut argv = vec!["lf".to_string(), "op".to_string(), item.command.clone()];
    argv.extend(item.args.iter().cloned());

    let cli = Cli::try_parse_from(argv)
        .map_err(|err| OpsError::Message(format!("invalid op item: {err}")))?;
    let op = match cli.command {
        Some(Commands::Op { op }) => op,
        _ => {
            return Err(OpsError::Message(
                "op item must parse as `lf op <command>`".to_string(),
            ));
        }
    };

    execute_parsed_ops(repo, &op, progress)
}

fn execute_parsed_ops(repo: &Path, op: &OpsCommand, progress: &impl Progress) -> OpsResult<()> {
    match op {
        OpsCommand::Land {
            strict,
            local,
            create_pr,
            worktree,
            message,
            title,
            body,
        } => {
            land(
                repo,
                &LandOptions {
                    strict: *strict,
                    local: *local,
                    create_pr: *create_pr,
                    worktree: worktree.clone(),
                    commit_message: message.clone(),
                    pr_title: title.clone(),
                    pr_body: body.clone(),
                    agent: None,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Submit {
            strict,
            create_pr,
            worktree,
            message,
            title,
            body,
        } => {
            submit(
                repo,
                &LandOptions {
                    strict: *strict,
                    local: false,
                    create_pr: *create_pr,
                    worktree: worktree.clone(),
                    commit_message: message.clone(),
                    pr_title: title.clone(),
                    pr_body: body.clone(),
                    agent: None,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Rebase { plan, onto } => {
            if *plan {
                return Ok(());
            }
            let base = get_default_branch(repo)?;
            let onto_ref = onto
                .as_ref()
                .map(ToString::to_string)
                .unwrap_or_else(|| format!("origin/{base}"));
            rebase_with_recovery(
                repo,
                &RebaseOptions {
                    onto: onto_ref,
                    push: true,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Pr {
            model: _,
            title,
            body,
        } => {
            create_or_update_pr(
                repo,
                &PrOptions {
                    title: title.clone(),
                    body: body.clone(),
                    agent: None,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Sync => {
            let base = get_default_branch(repo)?;
            if !sync_main(repo, &base)? {
                return Err(OpsError::Message(
                    "working tree dirty; sync aborted".to_string(),
                ));
            }
            Ok(())
        }
        OpsCommand::SyncSkills {
            global,
            yes: _,
            no_prune,
        } => {
            sync_skills(
                repo,
                &SkillSyncOptions {
                    include_global: *global,
                    prune: !*no_prune,
                    global_home: None,
                },
            )?;
            Ok(())
        }
        OpsCommand::Advance { wave } => {
            let wave = crate::ops::util::resolve_wave_name(repo, wave.as_deref())
                .ok_or_else(|| OpsError::Message("cannot determine wave name".to_string()))?;
            let branch = crate::ops::advance_branch(repo, &wave)?;
            progress.status(&format!("Advanced to branch: {branch}"));
            Ok(())
        }
        OpsCommand::Next {
            create_pr,
            no_rebase,
        } => {
            next_branch(
                repo,
                &NextOptions {
                    create_pr: *create_pr,
                    rebase: !*no_rebase,
                    wave_name: None,
                    agent: None,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Commit {
            message,
            push,
            no_add,
        } => {
            commit_workflow(
                repo,
                &CommitOptions {
                    add: !*no_add,
                    push: *push,
                    create_draft_pr: true,
                    message: message.clone(),
                    ..CommitOptions::for_task("commit")
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Abandon { force, branch } => {
            abandon_branch(
                repo,
                &AbandonOptions {
                    branch: branch.clone(),
                    force: *force,
                },
                progress,
            )?;
            Ok(())
        }
        OpsCommand::Release { cmd } => match cmd {
            ReleaseCommand::Run { version, target } => {
                release_run(
                    repo,
                    version.as_deref().unwrap_or("patch"),
                    target.as_deref(),
                    progress,
                )?;
                Ok(())
            }
            ReleaseCommand::Check { target } => {
                release_check(repo, target.as_deref())?;
                Ok(())
            }
            ReleaseCommand::Notes {
                version,
                prev_tag,
                target,
            } => {
                release_notes(
                    repo,
                    version,
                    prev_tag.as_deref(),
                    target.as_deref(),
                    progress,
                )?;
                Ok(())
            }
            ReleaseCommand::Bump { version, target } => {
                release_bump(repo, version, target.as_deref(), progress)
            }
            ReleaseCommand::Tag { version, target } => {
                release_tag(repo, version, target.as_deref())?;
                Ok(())
            }
            ReleaseCommand::Status { target } => {
                release_status(repo, target.as_deref())?;
                Ok(())
            }
        },
        OpsCommand::Push { force } => crate::engine::git::push(repo, *force).map_err(Into::into),
        OpsCommand::Cp { .. }
        | OpsCommand::Doctor { .. }
        | OpsCommand::Pm { .. }
        | OpsCommand::Branches { .. }
        | OpsCommand::Wt { .. }
        | OpsCommand::Shell { .. }
        | OpsCommand::Auth { .. }
        | OpsCommand::Queue { .. } => Err(OpsError::Message(
            "ops item does not support cp/doctor/pm/branches/wt/shell/auth/queue commands"
                .to_string(),
        )),
    }
}