systemprompt-cli 0.6.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::path::Path;
use systemprompt_content::{CategoryIdUpdate, ContentRepository};
use systemprompt_identifiers::CategoryId;

use super::edit::EditArgs;

const VALID_KINDS: &[&str] = &["article", "paper", "guide", "tutorial"];

pub struct ContentEditState {
    pub title: String,
    pub description: String,
    pub body: String,
    pub keywords: String,
    pub image: Option<String>,
    pub category_id: CategoryIdUpdate,
    pub public_value: Option<bool>,
    pub kind_value: Option<String>,
}

pub fn apply_visibility_flags(
    args: &EditArgs,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
) {
    if args.public {
        state.public_value = Some(true);
        changes.push("public: true".to_string());
    }
    if args.private {
        state.public_value = Some(false);
        changes.push("public: false".to_string());
    }
}

pub fn apply_body_flags(
    args: &EditArgs,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
) -> Result<()> {
    if let Some(b) = &args.body {
        state.body.clone_from(b);
        changes.push("body: updated".to_string());
    }
    if let Some(file) = &args.body_file {
        let path = Path::new(file);
        state.body = fs::read_to_string(path)
            .with_context(|| format!("Failed to read body file: {}", path.display()))?;
        changes.push("body: updated from file".to_string());
    }
    Ok(())
}

pub async fn apply_set_value_flags(
    args: &EditArgs,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
    repo: &ContentRepository,
) -> Result<()> {
    for set_value in &args.set_values {
        let parts: Vec<&str> = set_value.splitn(2, '=').collect();
        if parts.len() != 2 {
            return Err(anyhow!(
                "Invalid --set format: '{}'. Expected key=value",
                set_value
            ));
        }
        let key = parts[0].trim();
        let value = parts[1].trim();
        apply_set_field(key, value, state, changes, repo).await?;
    }
    Ok(())
}

async fn apply_set_field(
    key: &str,
    value: &str,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
    repo: &ContentRepository,
) -> Result<()> {
    match key {
        "title" => {
            state.title = value.to_string();
            changes.push(format!("title: {}", value));
        },
        "description" => {
            state.description = value.to_string();
            changes.push(format!("description: {}", value));
        },
        "keywords" => {
            state.keywords = value.to_string();
            changes.push(format!("keywords: {}", value));
        },
        "image" => apply_image_field(value, state, changes),
        "category_id" | "category" => apply_category_field(value, state, changes, repo).await?,
        "kind" => apply_kind_field(value, state, changes)?,
        "public" => apply_public_field(value, state, changes)?,
        _ => {
            return Err(anyhow!(
                "Unknown field: '{}'. Supported fields: title, description, keywords, image, \
                 category_id, kind, public",
                key
            ));
        },
    }
    Ok(())
}

fn apply_image_field(value: &str, state: &mut ContentEditState, changes: &mut Vec<String>) {
    if value.eq_ignore_ascii_case("none") || value.is_empty() {
        state.image = None;
        changes.push("image: cleared".to_string());
    } else {
        state.image = Some(value.to_string());
        changes.push(format!("image: {}", value));
    }
}

async fn apply_category_field(
    value: &str,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
    repo: &ContentRepository,
) -> Result<()> {
    if value.eq_ignore_ascii_case("none") || value.is_empty() {
        state.category_id = CategoryIdUpdate::Clear;
        changes.push("category_id: cleared".to_string());
        return Ok(());
    }
    let cat_id = CategoryId::new(value.to_string());
    if !repo.category_exists(&cat_id).await? {
        return Err(anyhow!(
            "Category '{}' not found. Please use an existing category ID.",
            value
        ));
    }
    state.category_id = CategoryIdUpdate::Set(cat_id);
    changes.push(format!("category_id: {}", value));
    Ok(())
}

fn apply_kind_field(
    value: &str,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
) -> Result<()> {
    if !VALID_KINDS.contains(&value) {
        return Err(anyhow!(
            "Invalid kind '{}'. Must be one of: {}",
            value,
            VALID_KINDS.join(", ")
        ));
    }
    state.kind_value = Some(value.to_string());
    changes.push(format!("kind: {}", value));
    Ok(())
}

fn apply_public_field(
    value: &str,
    state: &mut ContentEditState,
    changes: &mut Vec<String>,
) -> Result<()> {
    let p = value.parse::<bool>().map_err(|_| {
        anyhow!(
            "Invalid boolean value for public: '{}'. Use true or false",
            value
        )
    })?;
    state.public_value = Some(p);
    changes.push(format!("public: {}", p));
    Ok(())
}