a3s 0.10.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::io::{self, Write};

use serde::Serialize;

use super::super::catalog::{self, ComponentKind};
use super::super::lifecycle::OperationRecord;
use super::super::plan::OperationPlanSet;
use super::super::state::{ComponentReport, Health, Presence};
use super::{ComponentBatchFailure, ComponentFailure};

pub(super) fn print_human_report(report: &ComponentReport) {
    println!(
        "{:<18} {:<11} {:<17} {:<12} SOURCE",
        "COMPONENT", "TYPE", "STATUS", "VERSION"
    );
    for component in &report.components {
        let status = match (component.presence, component.health) {
            (Presence::Missing, _) => "missing".to_string(),
            (_, Health::Broken) => "broken".to_string(),
            (Presence::System, Health::Ready) => "ready (system)".to_string(),
            (Presence::External, Health::Ready) => "ready (external)".to_string(),
            (_, Health::Ready) => "ready".to_string(),
            _ => "unknown".to_string(),
        };
        let source = component
            .provenance
            .map(|value| format!("{value:?}").to_ascii_lowercase())
            .unwrap_or_else(|| "-".to_string());
        println!(
            "{:<18} {:<11} {:<17} {:<12} {}",
            component.id,
            format!("{:?}", component.kind).to_ascii_lowercase(),
            status,
            component.version.as_deref().unwrap_or("-"),
            source
        );
        if let Some(message) = &component.message {
            println!("  note: {message}");
        }
    }
    if !report.external_tools.is_empty() {
        println!();
        println!("EXTERNAL TOOLS (discovered, never activated automatically)");
        for tool in &report.external_tools {
            println!(
                "  {:<16} {:<20} {}",
                tool.command,
                tool.binary,
                tool.path.display()
            );
        }
    }
}

pub(super) fn print_available(json: bool) -> anyhow::Result<()> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Available<'a> {
        schema_version: u32,
        components: Vec<AvailableItem<'a>>,
    }
    #[derive(Serialize)]
    struct AvailableItem<'a> {
        id: &'a str,
        kind: ComponentKind,
        description: &'a str,
    }
    let output = Available {
        schema_version: 1,
        components: catalog::all()
            .iter()
            .map(|component| AvailableItem {
                id: component.id,
                kind: component.kind,
                description: component.description,
            })
            .collect(),
    };
    if json {
        print_json("component.install", &output, true)?;
    } else {
        println!("Available A3S components:");
        for component in catalog::all() {
            println!("  {:<18} {}", component.id, component.description);
        }
    }
    Ok(())
}

fn print_operations(
    command: &'static str,
    plan_digest: &str,
    operations: &[OperationRecord],
    json: bool,
) -> anyhow::Result<()> {
    if json {
        print_json(
            command,
            &serde_json::json!({"planDigest": plan_digest, "operations": operations}),
            true,
        )?;
    } else {
        println!("plan digest: {plan_digest}");
        for operation in operations {
            let marker = if operation.changed { "" } else { "=" };
            println!("{marker} {}", operation.message);
        }
    }
    Ok(())
}

pub(super) fn finish_batch(
    command: &'static str,
    action: &'static str,
    plan_digest: String,
    operations: Vec<OperationRecord>,
    failures: Vec<ComponentFailure>,
    json: bool,
) -> anyhow::Result<()> {
    if failures.is_empty() {
        return print_operations(command, &plan_digest, &operations, json);
    }
    if !json && !operations.is_empty() {
        print_operations(command, &plan_digest, &operations, false)?;
    }
    Err(ComponentBatchFailure {
        action,
        plan_digest,
        operations,
        failures,
    }
    .into())
}

pub(super) fn print_plans(
    command: &'static str,
    plan_set: &OperationPlanSet,
    json: bool,
) -> anyhow::Result<()> {
    if json {
        print_json(
            command,
            &serde_json::json!({
                "dryRun": true,
                "planSchemaVersion": plan_set.plan_schema_version,
                "planCommand": plan_set.plan_command,
                "planDigest": plan_set.plan_digest,
                "plans": plan_set.plans,
            }),
            true,
        )?;
    } else {
        plan_set.print_human();
    }
    Ok(())
}

pub(super) fn print_json(
    command: &'static str,
    data: &impl Serialize,
    ok: bool,
) -> anyhow::Result<()> {
    let value = serde_json::json!({
        "schemaVersion": 1,
        "command": command,
        "ok": ok,
        "data": data,
        "warnings": [],
    });
    let bytes = serde_json::to_vec_pretty(&value)?;
    let stdout = io::stdout();
    let mut stdout = stdout.lock();
    stdout.write_all(&bytes)?;
    stdout.write_all(b"\n")?;
    stdout.flush()?;
    Ok(())
}