a3s 0.9.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::{Path, PathBuf};

use serde_json::json;

use crate::commands::code::asset_types::AssetCommandOutput;
use crate::tui::{asset_clone, panels};

use super::output::{
    lifecycle_data, open_if_requested, os_asset_category_query, os_note, path_value,
    runtime_asset_query, trim_col,
};
use super::resolve::{
    read_flow_design, resolve_agent_dev, resolve_flow_file, resolve_mcp_dev, resolve_okf_dev,
    resolve_skill_dev,
};
use super::AssetCommandContext;

pub(super) async fn clone_asset(
    family: &'static str,
    url: String,
    root: PathBuf,
) -> anyhow::Result<AssetCommandOutput> {
    let result = asset_clone::clone_asset_source(family, url, root)
        .await
        .map_err(anyhow::Error::msg)?;
    let human = format!(
        "cloned {} asset source\nurl: {}\npath: {}\n",
        result.family,
        result.url,
        result.path.display()
    );
    Ok(AssetCommandOutput::new(
        json!({
            "family": result.family,
            "url": result.url,
            "path": path_value(&result.path),
        }),
        human,
    ))
}

pub(super) async fn list_assets(
    family: &'static str,
    category: &str,
    query: &str,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let session = load_os_session(context).await?;
    let scoped_query = os_asset_category_query(category, query);
    let result = panels::asset_resources::fetch_asset_list(
        &session.address,
        &session.access_token,
        &scoped_query,
    )
    .await
    .map_err(anyhow::Error::msg)?;
    let mut human = format!("{}\n", result.note);
    if result.rows.is_empty() {
        human.push_str("(no assets)\n");
    } else {
        human.push_str(&format!(
            "{:<28} {:<30} {:<12} {:<14} {:<12} updated\n",
            "id", "name", "category", "kind", "status"
        ));
    }
    for row in &result.rows {
        human.push_str(&format!(
            "{:<28} {:<30} {:<12} {:<14} {:<12} {}\n",
            trim_col(&row.id, 28),
            trim_col(&row.name, 30),
            trim_col(&row.category, 12),
            trim_col(&row.kind, 14),
            trim_col(&row.status, 12),
            row.updated
        ));
    }
    let rows = result
        .rows
        .iter()
        .map(|row| {
            json!({
                "id": row.id,
                "name": row.name,
                "category": row.category,
                "kind": row.kind,
                "status": row.status,
                "visibility": row.visibility,
                "owner": row.owner,
                "updated": row.updated,
                "accessUrl": row.access_url,
            })
        })
        .collect::<Vec<_>>();
    Ok(AssetCommandOutput::new(
        json!({
            "family": family,
            "location": "os",
            "query": query,
            "scopedQuery": scoped_query,
            "note": result.note,
            "assets": rows,
        }),
        human,
    ))
}

pub(super) async fn runtime_activity(
    family: &'static str,
    category: &str,
    query: &str,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let session = load_os_session(context).await?;
    let scoped_query = runtime_asset_query(category, "", query);
    let result = panels::asset_resources::fetch_runtime_activity(
        &session.address,
        &session.access_token,
        &scoped_query,
    )
    .await
    .map_err(anyhow::Error::msg)?;
    let mut human = format!("{}\n", result.note);
    if result.rows.is_empty() {
        human.push_str("(no runtime activity)\n");
    } else {
        human.push_str(&format!(
            "{:<28} {:<30} {:<12} {:<14} {:<12} updated\n",
            "id", "name", "category", "kind", "status"
        ));
    }
    for row in &result.rows {
        human.push_str(&format!(
            "{:<28} {:<30} {:<12} {:<14} {:<12} {}\n",
            trim_col(&row.id, 28),
            trim_col(&row.name, 30),
            trim_col(&row.asset_category, 12),
            trim_col(&row.kind, 14),
            trim_col(&row.status, 12),
            row.updated
        ));
    }
    let rows = result
        .rows
        .iter()
        .map(|row| {
            json!({
                "id": row.id,
                "name": row.name,
                "assetCategory": row.asset_category,
                "kind": row.kind,
                "status": row.status,
                "image": row.image,
                "accessUrl": row.access_url,
                "updated": row.updated,
                "source": row.source,
            })
        })
        .collect::<Vec<_>>();
    Ok(AssetCommandOutput::new(
        json!({
            "family": family,
            "query": query,
            "scopedQuery": scoped_query,
            "note": result.note,
            "activity": rows,
        }),
        human,
    ))
}

pub(super) async fn run_agent_os(
    action: panels::agent::AgentOsAction,
    path_arg: Option<&Path>,
    open_requested: bool,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let dev = resolve_agent_dev(path_arg.map(Path::to_path_buf), context)?;
    let session = load_os_session(context).await?;
    let result = panels::agent::publish_agent_to_os(session, dev, action)
        .await
        .map_err(anyhow::Error::msg)?;
    let opened_with = open_if_requested(open_requested && context.interactive, &result.view)?;
    let human = format!(
        "agent {} {}: {} ({})\n{}",
        result.action.label(),
        result.kind.label(),
        result.asset_name,
        result.asset_id,
        os_note(&result.note, &result.view, opened_with.as_deref())
    );
    Ok(AssetCommandOutput::new(
        lifecycle_data(
            "agent",
            result.action.label(),
            Some(result.kind.label()),
            &result.asset_name,
            &result.asset_id,
            &result.note,
            &result.view,
            opened_with,
        ),
        human,
    ))
}

pub(super) async fn run_mcp_os(
    action: panels::mcp::McpOsAction,
    path_arg: Option<&Path>,
    open_requested: bool,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let dev = resolve_mcp_dev(path_arg.map(Path::to_path_buf), context)?;
    let session = load_os_session(context).await?;
    let result = panels::mcp::publish_mcp_to_os(session, dev, action)
        .await
        .map_err(anyhow::Error::msg)?;
    let opened_with = open_if_requested(open_requested && context.interactive, &result.view)?;
    let human = format!(
        "mcp {}: {} ({})\n{}",
        result.action.label(),
        result.asset_name,
        result.asset_id,
        os_note(&result.note, &result.view, opened_with.as_deref())
    );
    Ok(AssetCommandOutput::new(
        lifecycle_data(
            "mcp",
            result.action.label(),
            None,
            &result.asset_name,
            &result.asset_id,
            &result.note,
            &result.view,
            opened_with,
        ),
        human,
    ))
}

pub(super) async fn run_skill_os(
    action: panels::skill::SkillOsAction,
    path_arg: Option<&Path>,
    open_requested: bool,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let dev = resolve_skill_dev(path_arg.map(Path::to_path_buf), context)?;
    let session = load_os_session(context).await?;
    let result = panels::skill::publish_skill_to_os(session, dev, action)
        .await
        .map_err(anyhow::Error::msg)?;
    let opened_with = open_if_requested(open_requested && context.interactive, &result.view)?;
    let human = format!(
        "skill {}: {} ({})\n{}",
        result.action.label(),
        result.asset_name,
        result.asset_id,
        os_note(&result.note, &result.view, opened_with.as_deref())
    );
    Ok(AssetCommandOutput::new(
        lifecycle_data(
            "skill",
            result.action.label(),
            None,
            &result.asset_name,
            &result.asset_id,
            &result.note,
            &result.view,
            opened_with,
        ),
        human,
    ))
}

pub(super) async fn run_flow_os(
    action: panels::flow::FlowOsAction,
    path_arg: Option<&Path>,
    open_requested: bool,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let flow = resolve_flow_file(path_arg.map(Path::to_path_buf), context)?;
    let design = read_flow_design(&flow.path)?;
    let session = load_os_session(context).await?;
    let result = panels::flow::publish_flow_to_os_with_local_path(
        session,
        flow.rel,
        Some(flow.path.clone()),
        design,
        action,
    )
    .await
    .map_err(anyhow::Error::msg)?;
    let opened_with = open_if_requested(open_requested && context.interactive, &result.view)?;
    let human = format!(
        "flow {}: {} ({})\n{}",
        result.action.label(),
        result.asset_name,
        result.asset_id,
        os_note(&result.note, &result.view, opened_with.as_deref())
    );
    Ok(AssetCommandOutput::new(
        lifecycle_data(
            "flow",
            result.action.label(),
            None,
            &result.asset_name,
            &result.asset_id,
            &result.note,
            &result.view,
            opened_with,
        ),
        human,
    ))
}

pub(super) async fn run_okf_os(
    action: panels::okf::OkfOsAction,
    path_arg: Option<&Path>,
    context: &AssetCommandContext,
) -> anyhow::Result<AssetCommandOutput> {
    let dev = resolve_okf_dev(path_arg.map(Path::to_path_buf), context)?;
    let session = load_os_session(context).await?;
    let result = panels::okf::publish_okf_to_os(session, dev, action)
        .await
        .map_err(anyhow::Error::msg)?;
    let human = format!(
        "okf {}: {} ({})\n{}",
        result.action.label(),
        result.asset_name,
        result.asset_id,
        os_note(&result.note, &result.view, None)
    );
    Ok(AssetCommandOutput::new(
        lifecycle_data(
            "okf",
            result.action.label(),
            None,
            &result.asset_name,
            &result.asset_id,
            &result.note,
            &result.view,
            None,
        ),
        human,
    ))
}

async fn load_os_session(
    context: &AssetCommandContext,
) -> anyhow::Result<crate::a3s_os::StoredOsSession> {
    let os_config = context.os_config.as_ref().ok_or_else(|| {
        anyhow::anyhow!("OS is not configured in the effective A3S ACL configuration")
    })?;
    let session = crate::a3s_os::current_session(os_config)
        .ok_or_else(|| anyhow::anyhow!("not signed in to OS; run `a3s auth login os`"))?;
    let session = if crate::a3s_os::needs_refresh(&session) {
        crate::a3s_os::refresh_session(&session).await?
    } else {
        session
    };
    Ok(session)
}