apm-cli 0.1.24

CLI project manager for running AI coding agents in parallel, isolated by design.
Documentation
use anyhow::{bail, Result};
use apm_core::{git, ticket, ticket_fmt};
use chrono::Utc;
use std::path::Path;
use crate::ctx::CmdContext;

pub fn run(root: &Path, id_arg: &str, field: String, value: String, no_aggressive: bool) -> Result<()> {
    let ctx = CmdContext::load(root, no_aggressive)?;
    let id = ticket::resolve_id_in_slice(&ctx.tickets, id_arg)?;
    let mut tickets = ctx.tickets;

    if field == "depends_on" && value != "-" {
        let ids: Vec<String> = value
            .split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        if !ids.is_empty() {
            let ticket = tickets.iter().find(|t| t.frontmatter.id == id)
                .ok_or_else(|| anyhow::anyhow!("ticket {id:?} not found"))?;
            let strategy = apm_core::validate::active_completion_strategy(&ctx.config);
            apm_core::validate::check_depends_on_rules(
                &strategy,
                ticket.frontmatter.epic.as_deref(),
                ticket.frontmatter.target_branch.as_deref(),
                &ids,
                &tickets,
                &ctx.config.project.default_branch,
            )?;
        }
    }

    let Some(t) = tickets.iter_mut().find(|t| t.frontmatter.id == id) else {
        bail!("ticket {id:?} not found");
    };
    if field == "owner" {
        ticket::check_owner(root, t)?;
        let local = apm_core::config::LocalConfig::load(root);
        apm_core::validate::validate_owner(&ctx.config, &local, &value)?;
    }
    if field == "agent" {
        apm_core::validate::validate_agent_name(&ctx.config, &value)?;
    }
    ticket::set_field(&mut t.frontmatter, &field, &value)?;
    t.frontmatter.updated_at = Some(Utc::now());

    let content = t.serialize()?;
    let rel_path = format!(
        "{}/{}",
        ctx.config.tickets.dir.to_string_lossy(),
        t.path.file_name().unwrap().to_string_lossy()
    );
    let branch = t
        .frontmatter
        .branch
        .clone()
        .or_else(|| ticket_fmt::branch_name_from_path(&t.path))
        .unwrap_or_else(|| format!("ticket/{id}"));

    git::commit_to_branch(
        root,
        &branch,
        &rel_path,
        &content,
        &format!("ticket({id}): set {field} = {value}"),
    )?;

    if ctx.aggressive {
        if let Err(e) = git::push_branch(root, &branch) {
            eprintln!("warning: push failed: {e:#}");
        }
    }

    println!("{id}: {field} = {value}");
    Ok(())
}