arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! `arc modules` — list application modules and their bindings
//! (side-effect-free).
//!
//! Loads the metadata artifact (no application boot) and prints a module
//! summary or JSON. Each module lists its imports, exports, controllers,
//! services, listeners, jobs, commands, and schedules. Supports `--json`.

use crate::cli::OutputFormat;
use crate::error::CommandError;
use crate::metadata::{ModuleEntry, Uag};
use crate::project;
use serde::Serialize;

pub(crate) fn execute(format: OutputFormat) -> Result<(), CommandError> {
    let project = project::discover()?;
    let artifact = crate::metadata::load(&project).map_err(CommandError::Metadata)?;
    let modules: Vec<&ModuleEntry> = artifact.modules.values().collect();
    match format {
        OutputFormat::Human => print_human(&modules),
        OutputFormat::Json => print_json(&artifact)?,
    }
    Ok(())
}

fn print_human(modules: &[&ModuleEntry]) {
    if modules.is_empty() {
        println!("no modules registered");
        return;
    }
    for module in modules {
        println!("== {} ==", module.name);
        println!("  imports:     {}", join_or_empty(&module.imports));
        println!("  exports:     {}", join_or_empty(&module.exports));
        println!("  controllers: {}", join_or_empty(&module.controllers));
        println!("  services:    {}", join_or_empty(&module.services));
        println!("  policies:    {}", join_or_empty(&module.policies));
        println!("  listeners:   {}", join_listeners(&module.listeners));
        println!("  jobs:        {}", join_jobs(&module.jobs));
        println!("  commands:    {}", join_commands(&module.commands));
        println!("  schedules:   {}", join_schedules(&module.schedules));
    }
}

fn print_json(artifact: &Uag) -> Result<(), CommandError> {
    // The UAG stores modules in a `BTreeMap<String, ModuleEntry>` (a JSON
    // object keyed by module name, for deterministic lookup); the focused
    // `arc modules --json` view emits them as an array of module entries, in
    // the map's already-sorted order, preserving the previous output shape.
    let modules: Vec<&ModuleEntry> = artifact.modules.values().collect();
    #[derive(Serialize)]
    struct Out<'a> {
        modules: Vec<&'a ModuleEntry>,
    }
    println!("{}", serde_json::to_string_pretty(&Out { modules })?);
    Ok(())
}

fn join_or_empty(items: &[String]) -> String {
    if items.is_empty() {
        "(none)".to_owned()
    } else {
        items.join(", ")
    }
}

fn join_listeners(listeners: &[crate::metadata::ListenerEntry]) -> String {
    if listeners.is_empty() {
        return "(none)".to_owned();
    }
    listeners
        .iter()
        .map(|l| format!("{} -> {}", l.event, l.listener))
        .collect::<Vec<_>>()
        .join(", ")
}

fn join_jobs(jobs: &[crate::metadata::JobEntry]) -> String {
    if jobs.is_empty() {
        return "(none)".to_owned();
    }
    jobs.iter()
        .map(|j| format!("{} v{} -> {}", j.kind, j.version, j.handler))
        .collect::<Vec<_>>()
        .join(", ")
}

fn join_commands(commands: &[crate::metadata::CommandEntry]) -> String {
    if commands.is_empty() {
        return "(none)".to_owned();
    }
    commands
        .iter()
        .map(|c| format!("{} -> {}", c.name, c.function))
        .collect::<Vec<_>>()
        .join(", ")
}

fn join_schedules(schedules: &[crate::metadata::ScheduleEntry]) -> String {
    if schedules.is_empty() {
        return "(none)".to_owned();
    }
    schedules
        .iter()
        .map(|s| format!("{} v{} -> {}", s.job, s.version, cadence_str(&s.cadence)))
        .collect::<Vec<_>>()
        .join(", ")
}

fn cadence_str(cadence: &crate::metadata::CadenceEntry) -> String {
    match cadence {
        crate::metadata::CadenceEntry::Every { seconds } => format!("every {seconds}s"),
        crate::metadata::CadenceEntry::Daily { hour, minute } => {
            format!("daily {hour:02}:{minute:02}")
        }
    }
}