apm-cli 0.1.46

CLI project manager for running AI coding agents in parallel, isolated by design.
Documentation
use anyhow::Result;
use apm_core::config::Config;
use apm_core::wrapper::WrapperKind;
use std::path::Path;
use apm_core::start::AgentDiagnostic;

pub fn run_list(root: &Path) -> Result<()> {
    let config = Config::load(root)?;
    let entries = apm_core::agents::list_wrappers(root, &config)?;

    let name_w = entries.iter().map(|e| e.name.len()).max().unwrap_or(4).max(4);
    let kind_w = "built-in".len();
    let parser_w = "canonical".len();

    println!(
        "{:<name_w$}  {:<kind_w$}  {:<parser_w$}  STATUS",
        "NAME", "KIND", "PARSER"
    );
    for entry in &entries {
        let kind_str = match &entry.kind {
            WrapperKind::Builtin(_) => "built-in",
            WrapperKind::Custom { .. } => "project",
        };
        let status = entry.configured_as.join(", ");
        println!(
            "{:<name_w$}  {:<kind_w$}  {:<parser_w$}  {}",
            entry.name, kind_str, entry.parser, status
        );
    }
    Ok(())
}

pub fn run_new(root: &Path, name: &str, force: bool) -> Result<()> {
    apm_core::agents::scaffold_wrapper(root, name, force)?;

    let dir = root.join(".apm").join("agents").join(name);
    println!("Created:");
    println!("  {}", dir.join("wrapper.sh").display());
    println!("  {}", dir.join("manifest.toml").display());
    println!("  {}", dir.join("apm.worker.md").display());
    println!("  {}", dir.join("apm.spec-writer.md").display());
    println!();
    println!("Next steps:");
    println!("  1. Edit {}/wrapper.sh to invoke your AI tool", dir.display());
    println!("  2. Run: apm agents test {name}");
    Ok(())
}

pub fn run_test(root: &Path, name: &str) -> Result<()> {
    let report = apm_core::agents::test_wrapper(root, name)?;

    let label = if report.passed { "PASS" } else { "FAIL" };
    println!(
        "{}  exit={}  events={}  non-canonical={}  stderr={}  wall={}ms",
        label,
        report.exit_code,
        report.canonical_events,
        report.non_canonical_lines,
        report.stderr_lines,
        report.wall_millis,
    );

    if !report.passed {
        anyhow::bail!("wrapper test failed");
    }
    Ok(())
}

pub fn run_eject(root: &Path, name: &str) -> Result<()> {
    apm_core::agents::eject_wrapper(root, name)?;

    let script = root.join(".apm").join("agents").join(name).join("wrapper.sh");
    println!("Ejected to: {}", script.display());
    println!("Run: apm agents test {name}");
    Ok(())
}

pub fn run_resolve(root: &Path, ticket_id: &str, json: bool) -> Result<()> {
    let diag = apm_core::start::resolve_for_diagnostic(root, ticket_id)?;
    if json {
        print_resolve_json(&diag)
    } else {
        print_resolve_human(&diag)
    }
}

fn print_resolve_human(d: &AgentDiagnostic) -> Result<()> {
    println!("Agent assignment for {} (state: {}):", d.ticket_id, d.ticket_state);
    if !d.dispatchable {
        println!("  note: state {:?} has no worker dispatch; showing resolution for {:?}", d.ticket_state, d.transition_label);
    }
    if d.transition_label == "none" {
        println!("  (no command:start transition defined in this workflow)");
        return Ok(());
    }

    const W: usize = 14;
    let model_val = d.model.as_deref().unwrap_or("");
    let container_val = d.container.as_deref().unwrap_or("");
    let manifest_status = if d.manifest_present { "[present]" } else { "[absent]" };

    println!("  {:<W$} {}  ({})", "agent", d.agent, d.agent_source);
    println!("  {:<W$} {}  ({})", "role", d.role, d.role_source);
    println!("  {:<W$} {}  ({})", "model", model_val, d.model_source);
    println!("  {:<W$} {}  ({})", "container", container_val, d.container_source);
    println!("  {:<W$} {}  {}", "manifest", d.manifest_path, manifest_status);

    if d.env.is_empty() {
        println!("  {:<W$} (none)", "env");
    } else {
        println!("  {:<W$}", "env");
        for (k, v, src) in &d.env {
            println!("    {k}={v}  ({src})");
        }
    }

    if d.keychain.is_empty() {
        println!("  {:<W$} (none)", "keychain");
    } else {
        println!("  {:<W$}", "keychain");
        let mut entries: Vec<_> = d.keychain.iter().collect();
        entries.sort_by_key(|(k, _)| k.as_str());
        for (k, v) in entries {
            println!("    {k}{v}");
        }
    }
    Ok(())
}

fn print_resolve_json(d: &AgentDiagnostic) -> Result<()> {
    let env_arr: Vec<serde_json::Value> = d.env.iter()
        .map(|(k, v, src)| serde_json::json!({"key": k, "value": v, "source": src}))
        .collect();
    let keychain_obj: serde_json::Map<String, serde_json::Value> = d.keychain.iter()
        .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
        .collect();
    let obj = serde_json::json!({
        "ticket_id": d.ticket_id,
        "ticket_state": d.ticket_state,
        "dispatchable": d.dispatchable,
        "resolved_from_state": d.resolved_from_state,
        "transition_label": d.transition_label,
        "worker_profile_str": d.worker_profile_str,
        "profile_source": d.profile_source,
        "agent": d.agent,
        "agent_source": d.agent_source,
        "role": d.role,
        "role_source": d.role_source,
        "model": d.model,
        "model_source": d.model_source,
        "container": d.container,
        "container_source": d.container_source,
        "manifest_path": d.manifest_path,
        "manifest_present": d.manifest_present,
        "env": env_arr,
        "keychain": keychain_obj,
    });
    println!("{}", serde_json::to_string_pretty(&obj)?);
    Ok(())
}