agileplus-cli 0.3.0

Typed intent graphs for engineering work — convert prompts into validated DAGs of intents, features, tasks, tests, and edges, then store, query, and serve them from Rust, HTTP, or MCP.
//! `agileplus tag` — attach or detach a tag to a stored graph.

use std::path::PathBuf;

use agileplus_sqlite::Storage;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};

/// Manage tags on a stored intent graph.
#[derive(Debug, Args)]
#[command(name = "tag")]
pub struct TagCommand {
    /// Path to the SQLite database.
    #[arg(short, long)]
    db: PathBuf,

    /// Graph id to operate on.
    #[arg(short, long)]
    id: String,

    #[command(subcommand)]
    action: TagAction,
}

#[derive(Debug, Subcommand)]
enum TagAction {
    /// Add a tag to a graph.
    Add {
        /// Tag text to attach.
        tag: String,
    },
    /// Remove a tag from a graph.
    Remove {
        /// Tag text to detach.
        tag: String,
    },
    /// List the tags attached to a graph.
    List,
}

impl TagCommand {
    pub fn run(&self) -> Result<()> {
        let storage = Storage::open(&self.db)
            .with_context(|| format!("Failed to open storage at: {}", self.db.display()))?;

        match &self.action {
            TagAction::Add { tag } => {
                let inserted = storage
                    .add_tag(&self.id, tag)
                    .with_context(|| format!("Failed to add tag '{tag}' to graph {}", self.id))?;
                if inserted {
                    println!("tagged: {} -> {tag}", self.id);
                } else {
                    println!("already tagged: {} -> {tag}", self.id);
                }
            }
            TagAction::Remove { tag } => {
                let removed = storage
                    .remove_tag(&self.id, tag)
                    .with_context(|| format!("Failed to remove tag '{tag}' from graph {}", self.id))?;
                if removed {
                    println!("untagged: {} -> {tag}", self.id);
                } else {
                    println!("not present: {} -> {tag}", self.id);
                }
            }
            TagAction::List => {
                let tags = storage
                    .list_tags(&self.id)
                    .with_context(|| format!("Failed to list tags for graph {}", self.id))?;
                if tags.is_empty() {
                    println!("(no tags on {})", self.id);
                } else {
                    println!("{} tag(s) on {}:", tags.len(), self.id);
                    for t in &tags {
                        println!("  {t}");
                    }
                }
            }
        }
        Ok(())
    }
}