a3s 0.10.1

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

use crate::tui::panels;

use super::AssetCommandContext;

pub(crate) fn resolve_agent_dev(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<panels::agent::AgentDevSession> {
    let config_root = &context.directories.agent;
    let target = resolve_optional_target(path_arg, context)?;
    let file = choose_agent_file(&target)?;
    let root = asset_root_for_file(config_root, &file);
    panels::agent::agent_dev_session_from_file(&root, &file).map_err(anyhow::Error::msg)
}

pub(super) fn resolve_mcp_dev(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<panels::mcp::McpDevSession> {
    let config_root = &context.directories.mcp;
    let target = asset_dir_target(resolve_optional_target(path_arg, context)?);
    let project = choose_mcp_project(&target, config_root)?;
    Ok(panels::mcp::McpDevSession {
        name: project.name,
        description: project.description,
        rel: project.rel,
        path: project.path,
        root: asset_root_for_dir(config_root, &target),
    })
}

pub(super) fn resolve_skill_dev(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<panels::skill::SkillDevSession> {
    let config_root = &context.directories.skill;
    let target = resolve_optional_target(path_arg, context)?;
    let file = choose_skill_file(&target)?;
    let root = asset_root_for_file(config_root, &file);
    Ok(panels::skill::skill_dev_session_from_file(&root, &file))
}

#[derive(Debug, Clone)]
pub(super) struct FlowFile {
    pub(super) rel: String,
    pub(super) path: PathBuf,
}

pub(super) fn resolve_flow_file(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<FlowFile> {
    let config_root = &context.directories.flow;
    let target = resolve_optional_target(path_arg, context)?;
    let file = choose_flow_file(&target)?;
    let root = asset_root_for_file(config_root, &file);
    let rel = rel_to_root(&root, &file);
    Ok(FlowFile { rel, path: file })
}

pub(crate) fn resolve_okf_dev(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<panels::okf::OkfDevSession> {
    let default_root = &context.directories.okf;
    let target = match path_arg {
        Some(path) => resolve_asset_path(&path, context)?,
        None => {
            if panels::okf::okf_package_asset_from_dir(&context.workspace, &context.workspace)
                .is_some()
            {
                context.workspace.clone()
            } else {
                default_root.clone()
            }
        }
    };
    let target = asset_dir_target(target);
    let package = choose_okf_package(&target, default_root)?;
    Ok(panels::okf::OkfDevSession {
        name: package.name,
        description: package.description,
        rel: package.rel,
        path: package.path,
        root: asset_root_for_dir(default_root, &target),
    })
}

fn choose_agent_file(target: &Path) -> anyhow::Result<PathBuf> {
    if target.is_file() {
        return Ok(target.to_path_buf());
    }
    if let Some(entry) = panels::agent::agent_entry_file(target) {
        return Ok(entry);
    }
    let agents = panels::agent::list_agents(target);
    match agents.as_slice() {
        [agent] => Ok(agent.definition_path.clone()),
        [] => anyhow::bail!("no agent package found in {}", target.display()),
        _ => anyhow::bail!(
            "multiple agent packages found in {}; pass one package path",
            target.display()
        ),
    }
}

fn choose_mcp_project(
    target: &Path,
    config_root: &Path,
) -> anyhow::Result<panels::mcp::McpProject> {
    let root = if target.is_dir() {
        asset_root_for_dir(config_root, target)
    } else {
        anyhow::bail!("MCP path must be a directory: {}", target.display());
    };
    let projects = panels::mcp::list_mcp_projects(target);
    match projects.as_slice() {
        [project] => {
            let rel = rel_to_root(&root, &project.path);
            Ok(panels::mcp::McpProject {
                rel,
                path: project.path.clone(),
                name: project.name.clone(),
                description: project.description.clone(),
            })
        }
        [] => anyhow::bail!("no MCP asset found in {}", target.display()),
        _ => anyhow::bail!(
            "multiple MCP assets found in {}; pass one asset directory",
            target.display()
        ),
    }
}

fn choose_skill_file(target: &Path) -> anyhow::Result<PathBuf> {
    if target.is_file() {
        return Ok(target.to_path_buf());
    }
    let skills = panels::skill::list_skill_assets(target);
    match skills.as_slice() {
        [skill] => Ok(skill.path.clone()),
        [] => anyhow::bail!("no skill asset found in {}", target.display()),
        _ => anyhow::bail!(
            "multiple skill assets found in {}; pass one skill file or directory",
            target.display()
        ),
    }
}

fn choose_flow_file(target: &Path) -> anyhow::Result<PathBuf> {
    if target.is_file() {
        return Ok(target.to_path_buf());
    }
    let flows = panels::flow::list_flows(target);
    match flows.as_slice() {
        [flow] => Ok(target.join(flow)),
        [] => anyhow::bail!("no workflow JSON found in {}", target.display()),
        _ => anyhow::bail!(
            "multiple workflow JSON files found in {}; pass one file path",
            target.display()
        ),
    }
}

fn choose_okf_package(
    target: &Path,
    default_root: &Path,
) -> anyhow::Result<panels::okf::OkfPackageAsset> {
    let root = asset_root_for_dir(default_root, target);
    if target.is_dir() {
        if let Some(package) = panels::okf::okf_package_asset_from_dir(&root, target) {
            return Ok(package);
        }
    }
    let packages = panels::okf::list_okf_packages(target);
    match packages.as_slice() {
        [package] => {
            let rel = rel_to_root(&root, &package.path);
            Ok(panels::okf::OkfPackageAsset {
                rel,
                path: package.path.clone(),
                name: package.name.clone(),
                description: package.description.clone(),
            })
        }
        [] => anyhow::bail!("no OKF package found in {}", target.display()),
        _ => anyhow::bail!(
            "multiple OKF packages found in {}; pass one package directory",
            target.display()
        ),
    }
}

fn asset_dir_target(path: PathBuf) -> PathBuf {
    if path.is_file() {
        return path
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or_else(|| path.clone());
    }
    path
}

fn resolve_optional_target(
    path_arg: Option<PathBuf>,
    context: &AssetCommandContext,
) -> anyhow::Result<PathBuf> {
    match path_arg {
        Some(path) => resolve_asset_path(&path, context),
        None => Ok(context.workspace.clone()),
    }
}

fn resolve_asset_path(path: &Path, context: &AssetCommandContext) -> anyhow::Result<PathBuf> {
    let expanded = if let Ok(rest) = path.strip_prefix(Path::new("~")) {
        context
            .home
            .as_deref()
            .map(|home| home.join(rest))
            .unwrap_or_else(|| path.to_path_buf())
    } else {
        path.to_path_buf()
    };
    let path = if expanded.is_absolute() {
        expanded
    } else {
        context.workspace.join(expanded)
    };
    std::fs::canonicalize(&path)
        .map_err(|e| anyhow::anyhow!("could not resolve {}: {e}", path.display()))
}

fn asset_root_for_file(config_root: &Path, file: &Path) -> PathBuf {
    let dir = file.parent().unwrap_or_else(|| Path::new("."));
    asset_root_for_dir(config_root, dir)
}

fn asset_root_for_dir(config_root: &Path, dir: &Path) -> PathBuf {
    if let (Ok(config_root), Ok(dir)) = (
        std::fs::canonicalize(config_root),
        std::fs::canonicalize(dir),
    ) {
        if dir.starts_with(&config_root) {
            return config_root;
        }
    }
    dir.to_path_buf()
}

fn rel_to_root(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .unwrap_or(path)
        .components()
        .map(|part| part.as_os_str().to_string_lossy())
        .collect::<Vec<_>>()
        .join("/")
}

pub(super) fn read_flow_design(path: &Path) -> anyhow::Result<String> {
    let design = std::fs::read_to_string(path)
        .map_err(|e| anyhow::anyhow!("could not read {}: {e}", path.display()))?;
    serde_json::from_str::<serde_json::Value>(&design)
        .map_err(|e| anyhow::anyhow!("{} is not valid workflow JSON: {e}", path.display()))?;
    Ok(design)
}