proto_cli 0.57.3

A multi-language version manager, a unified toolchain.
use crate::session::ProtoSession;
use clap::Args;
use iocraft::prelude::element;
use proto_core::flow::resolve::Resolver;
use proto_core::{PinLocation, ProtoConfig, ProtoConfigError, Tool, ToolContext, ToolSpec, cfg};
use proto_pdk_api::{PinVersionInput, PinVersionOutput, PluginFunction};
use starbase::AppResult;
use starbase_console::ui::*;
use starbase_styles::encode_style_tags;
use std::path::PathBuf;
use tracing::debug;

#[derive(Args, Clone, Debug)]
pub struct PinArgs {
    #[arg(required = true, help = "Tool to pin")]
    pub context: ToolContext,

    #[arg(required = true, help = "Version specification to pin")]
    pub spec: ToolSpec,

    #[arg(long, help = "Resolve the version before pinning")]
    pub resolve: bool,

    #[arg(long, default_value_t, help = "Directory location to pin to")]
    pub to: PinLocation,

    #[arg(long, help = "Pin to the tool's native file instead of .prototools")]
    pub tool_native: bool,
}

pub async fn internal_pin(
    tool: &Tool,
    spec: &ToolSpec,
    pin_to: PinLocation,
) -> Result<PathBuf, ProtoConfigError> {
    let version = match &spec.version {
        Some(version) => version.to_string(),
        None => spec.req.to_string(),
    };

    let config_path = ProtoConfig::update_document(tool.proto.get_config_dir(pin_to), |doc| {
        doc[tool.context.as_str()] = cfg::value(&version);
    })?;

    debug!(
        tool = tool.context.as_str(),
        version = &version,
        config = ?config_path,
        "Pinned the version",
    );

    Ok(config_path)
}

#[tracing::instrument(skip_all)]
pub async fn pin(session: ProtoSession, args: PinArgs) -> AppResult {
    let mut spec = args.spec.clone();
    let tool = session.load_tool(&args.context).await?;

    if args.resolve {
        Resolver::resolve(&tool, &mut spec, false).await?;
    }

    let config_path;

    if args.tool_native {
        if tool.plugin.has_func(PluginFunction::PinVersion).await {
            let output: PinVersionOutput = tool
                .plugin
                .call_func_with(
                    PluginFunction::PinVersion,
                    PinVersionInput {
                        context: tool.create_plugin_unresolved_context(),
                        dir: tool.to_virtual_path(tool.proto.get_config_dir(args.to)),
                        version: spec.to_unresolved_spec(),
                    },
                )
                .await?;

            if let Some(file) = output.file
                && output.pinned
            {
                config_path = tool.from_virtual_path(file);
            } else {
                session.console.render_err(element! {
                    Notice(variant: Variant::Failure) {
                        StyledText(
                            content: format!(
                                "Failed to pin version <version>{}</version> for <id>{}</id>.",
                                encode_style_tags(spec.to_string()),
                                args.context,
                            )
                        )
                        #(output.error.map(|error| {
                            element! {
                                StyledText(content: error)
                            }
                        }))
                    }
                })?;

                return Ok(Some(1));
            }
        } else {
            session.console.render_err(element! {
                Notice(variant: Variant::Caution) {
                    StyledText(
                        content: format!(
                            "{} does not support pinning to a native file. Remove <shell>--tool-native</shell> and try again.",
                            tool.get_name()
                        )
                    )
                }
            })?;

            return Ok(Some(1));
        }
    } else {
        config_path = internal_pin(&tool, &spec, args.to).await?;
    }

    session.console.render(element! {
        Notice(variant: Variant::Success) {
            StyledText(
                content: if args.resolve {
                    format!(
                        "Pinned <id>{}</id> version <version>{}</version> (resolved from <versionalt>{}</versionalt>) to config <path>{}</path>",
                        args.context,
                        spec.get_resolved_version(),
                        encode_style_tags(spec.req.to_string()),
                        config_path.display()
                    )
                } else {
                    format!(
                        "Pinned <id>{}</id> version <version>{}</version> to config <path>{}</path>",
                        args.context,
                        encode_style_tags(spec.req.to_string()),
                        config_path.display()
                    )
                },
            )
        }
    })?;

    Ok(None)
}