openlatch-provider 0.1.0

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
//! `tools list / delete / deprecate`.

use crate::api::editor::{delete_tool, deprecate_tool, list_tools};
use crate::cli::commands::shared::{hard_confirm, make_client};
use crate::cli::{GlobalArgs, ToolsAction};
use crate::error::{OlError, OL_4200_TOKEN_EXPIRED};
use crate::ui::output::OutputConfig;
use clap::Args;
use tabled::Table;

#[derive(Args, Debug)]
pub struct DeprecateArgs {
    /// Tool slug (or `<slug>@<range>` when only a version range is targeted).
    pub spec: String,
    /// Deprecation message shown to consumers.
    pub message: String,
}

pub async fn run(g: &GlobalArgs, action: ToolsAction) -> Result<(), OlError> {
    match action {
        ToolsAction::List => list(g).await,
        ToolsAction::Delete { slug, yes } => delete(g, &slug, yes).await,
        ToolsAction::Deprecate(args) => deprecate_body(g, args).await,
    }
}

/// Public entry-point used by the top-level `Deprecate` subcommand. Wraps
/// the same body as `tools deprecate`.
pub async fn deprecate(g: &GlobalArgs, args: DeprecateArgs) -> Result<(), OlError> {
    deprecate_body(g, args).await
}

async fn list(g: &GlobalArgs) -> Result<(), OlError> {
    let out = OutputConfig::resolve(g);
    let client = make_client().await?;
    let tools = list_tools(&client).await?;
    if out.is_machine() {
        out.print_json(&tools);
        return Ok(());
    }
    if tools.is_empty() {
        out.print_info("No tools found.");
        return Ok(());
    }
    let rows: Vec<crate::ui::tables::ToolRow> = tools
        .into_iter()
        .map(|t| crate::ui::tables::ToolRow {
            slug: t.slug,
            version: t.version.unwrap_or_else(|| "(none)".into()),
            state: t.lifecycle_state.unwrap_or_else(|| "active".into()),
            routing_score: t
                .routing_score
                .map(|s| format!("{s:.1}"))
                .unwrap_or_else(|| "".into()),
        })
        .collect();
    println!("{}", Table::new(rows));
    Ok(())
}

async fn delete(g: &GlobalArgs, slug: &str, yes_flag: bool) -> Result<(), OlError> {
    let out = OutputConfig::resolve(g);
    let yes = g.yes || yes_flag;
    if g.dry_run {
        out.print_step(&format!(
            "--dry-run: would DELETE /api/v1/editor/tools/{slug}"
        ));
        return Ok(());
    }
    hard_confirm(slug, out.interactive, yes)?;
    let client = make_client().await?;
    match delete_tool(&client, slug).await {
        Ok(()) => {
            out.print_step(&format!("Deleted tool `{slug}`"));
            Ok(())
        }
        Err(e) if e.code.code == "OL-4234" => {
            // Idempotent — already gone.
            out.print_step(&format!("Tool `{slug}` already absent — no-op"));
            Ok(())
        }
        Err(e) => Err(e),
    }
}

async fn deprecate_body(g: &GlobalArgs, args: DeprecateArgs) -> Result<(), OlError> {
    let out = OutputConfig::resolve(g);
    let slug = args.spec.split('@').next().unwrap_or("");
    if slug.is_empty() {
        return Err(OlError::new(
            OL_4200_TOKEN_EXPIRED,
            format!(
                "invalid tool spec `{}` (expected slug or slug@range)",
                args.spec
            ),
        ));
    }
    if g.dry_run {
        out.print_step(&format!(
            "--dry-run: would PATCH /api/v1/editor/tools/{slug} (lifecycle_state=deprecated)"
        ));
        return Ok(());
    }
    let client = make_client().await?;
    let resp = deprecate_tool(&client, slug, &args.message).await?;
    if out.is_machine() {
        out.print_json(&resp);
    } else {
        out.print_step(&format!(
            "Marked `{slug}` deprecated with message: {}",
            args.message
        ));
    }
    Ok(())
}