harn-cli 0.10.23

CLI for the Harn programming language — run, test, REPL, format, and lint
use std::path::Path;
use std::str::FromStr;

use harn_modules::personas::PersonaAutonomyTier;

use crate::cli::{PersonaActivateArgs, PersonaActivationsArgs, PersonaDeactivateArgs};
use crate::package::{self, PersonaActivationReceipt, PersonaActivationRecord, PersonaAttenuation};

pub(crate) fn run_activate(
    manifest: Option<&Path>,
    args: &PersonaActivateArgs,
) -> Result<(), String> {
    let receipt = activate_payload(manifest, args)?;
    print_receipt(&receipt, args.json)
}

pub(crate) fn activate_payload(
    manifest: Option<&Path>,
    args: &PersonaActivateArgs,
) -> Result<PersonaActivationReceipt, String> {
    let attenuation = attenuation_from_args(args)?;
    let now_ms = super::persona::timestamp_arg(args.at.as_deref())?;
    package::activate_persona(manifest, &args.name, &attenuation, now_ms)
        .map_err(|error| error.to_string())
}

pub(crate) fn run_deactivate(
    manifest: Option<&Path>,
    args: &PersonaDeactivateArgs,
) -> Result<(), String> {
    let receipt = deactivate_payload(manifest, args)?;
    print_receipt(&receipt, args.json)
}

pub(crate) fn deactivate_payload(
    manifest: Option<&Path>,
    args: &PersonaDeactivateArgs,
) -> Result<PersonaActivationReceipt, String> {
    let now_ms = super::persona::timestamp_arg(args.at.as_deref())?;
    package::deactivate_persona(manifest, &args.name, now_ms).map_err(|error| error.to_string())
}

pub(crate) fn run_activations(
    manifest: Option<&Path>,
    args: &PersonaActivationsArgs,
) -> Result<(), String> {
    let activations = activations_payload(manifest)?;
    if args.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&activations).map_err(|error| error.to_string())?
        );
    } else {
        print_activations(&activations);
    }
    Ok(())
}

pub(crate) fn activations_payload(
    manifest: Option<&Path>,
) -> Result<Vec<PersonaActivationRecord>, String> {
    package::list_persona_activations(manifest).map_err(|error| error.to_string())
}

fn attenuation_from_args(args: &PersonaActivateArgs) -> Result<PersonaAttenuation, String> {
    let autonomy_tier = args
        .autonomy_tier
        .as_deref()
        .map(PersonaAutonomyTier::from_str)
        .transpose()
        .map_err(|()| "invalid autonomy tier".to_string())?;
    Ok(PersonaAttenuation {
        autonomy_tier,
        tools: selected_set(&args.tools, args.no_tools),
        capabilities: selected_set(&args.capabilities, args.no_capabilities),
    })
}

fn selected_set(values: &[String], deny_all: bool) -> Option<Vec<String>> {
    if deny_all {
        Some(Vec::new())
    } else if values.is_empty() {
        None
    } else {
        Some(values.to_vec())
    }
}

fn print_receipt(receipt: &PersonaActivationReceipt, json: bool) -> Result<(), String> {
    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(receipt).map_err(|error| error.to_string())?
        );
    } else {
        println!(
            "{}: {} changed={} ledger={}",
            match receipt.action {
                package::PersonaActivationAction::Activate => "activated",
                package::PersonaActivationAction::Deactivate => "deactivated",
            },
            receipt.persona_id,
            receipt.changed,
            receipt.ledger_path
        );
    }
    Ok(())
}

fn print_activations(activations: &[PersonaActivationRecord]) {
    if activations.is_empty() {
        println!("No installed persona activations found.");
        return;
    }
    println!("Installed persona activations:");
    for activation in activations {
        let status = activation
            .migration
            .as_ref()
            .map(|_| " reactivation-required")
            .unwrap_or("");
        println!(
            "  {}  package={} digest={} autonomy={}{}",
            activation.persona_id,
            activation.package.alias,
            activation.package.content_hash,
            activation.effective_policy.autonomy_tier.as_str(),
            status
        );
    }
}