ggen-cli-lib 26.7.3

CLI interface for ggen
Documentation
//! Capability noun — resolve and enable capability surfaces
//! (`ggen capability <verb>`).
//!
//! A capability (e.g. `mcp`, `web`, `devops`) expands to a set of *atomic packs*.
//! `enable` records those packs in the project lockfile so a subsequent
//! `ggen sync` can generate from them; `list` and `inspect` are read-only
//! discovery. This is the capability-oriented entry to the same lockfile the
//! `packs` noun manages, and a natural first step for an agent bringing a project
//! up: pick a capability, enable it, then sync.

use clap_noun_verb::{NounVerbError, Result};
use clap_noun_verb_macros::verb;
use serde_json::{json, Value};
use std::path::PathBuf;

use ggen_core::domain::packs::capability_registry::{
    list_capabilities, resolve_capability_to_packs,
};
use ggen_core::packs::lockfile::{LockedPack, PackLockfile, PackSource};

// ── helpers ─────────────────────────────────────────────────────────────────

fn project_root() -> Result<PathBuf> {
    std::env::current_dir()
        .map_err(|e| NounVerbError::execution_error(format!("cannot resolve project dir: {}", e)))
}

/// Resolve the atomic packs for a capability surface, appending a
/// `projection-<proj>` pack when `--projection` is supplied.
fn atomic_packs_for(surface: &str, projection: Option<&str>, runtime: Option<&str>) -> Vec<String> {
    let mut packs = resolve_capability_to_packs(surface, projection, runtime).unwrap_or_default();
    if let Some(p) = projection {
        let projection_pack = format!("projection-{}", p);
        if !packs.contains(&projection_pack) {
            packs.push(projection_pack);
        }
    }
    packs
}

fn require_surface(surface: &str) -> Result<()> {
    if surface.trim().is_empty() {
        return Err(NounVerbError::argument_error(
            "capability surface must not be empty",
        ));
    }
    Ok(())
}

// ── verbs ───────────────────────────────────────────────────────────────────

/// Enable a capability: expand it to atomic packs and record them in the project
/// lockfile, returning the expansion as JSON.
#[verb]
pub fn enable(
    #[arg(index = 1)] surface: String, projection: Option<String>, runtime: Option<String>,
) -> Result<Value> {
    require_surface(&surface)?;
    let packs = atomic_packs_for(&surface, projection.as_deref(), runtime.as_deref());

    // Record each atomic pack as a declared lockfile entry.
    let root = project_root()?;
    let lock_path = root.join(".ggen").join("packs.lock");
    let mut lockfile = if lock_path.exists() {
        PackLockfile::from_file(&lock_path)
            .map_err(|e| NounVerbError::execution_error(format!("cannot read lockfile: {}", e)))?
    } else {
        PackLockfile::new(env!("CARGO_PKG_VERSION"))
    };
    for pid in &packs {
        let digest = ggen_core::calculate_sha256(format!("{}@0.0.0", pid).as_bytes());
        lockfile.add_pack(
            pid,
            LockedPack {
                version: "0.0.0".to_string(),
                source: PackSource::Registry {
                    url: "https://registry.ggen.io".to_string(),
                },
                integrity: Some(format!("sha256-{}", digest)),
                installed_at: chrono::Utc::now(),
                dependencies: Vec::new(),
            },
        );
    }
    lockfile
        .save(&lock_path)
        .map_err(|e| NounVerbError::execution_error(format!("cannot write lockfile: {}", e)))?;

    Ok(json!({
        "capability": surface,
        "projection": projection,
        "runtime": runtime,
        "atomic_packs": packs,
        "lockfile": lock_path.display().to_string(),
    }))
}

/// List the known capability surfaces.
#[verb]
pub fn list() -> Result<Value> {
    let caps: Vec<Value> = list_capabilities()
        .into_iter()
        .map(|c| {
            json!({
                "id": c.id,
                "name": c.name,
                "description": c.description,
                "category": c.category,
                "atomic_packs": c.atomic_packs,
            })
        })
        .collect();
    Ok(json!({ "total": caps.len(), "capabilities": caps }))
}

/// Inspect a capability surface: show the atomic packs it expands to.
#[verb]
pub fn inspect(#[arg(index = 1)] surface: String) -> Result<Value> {
    require_surface(&surface)?;
    let packs = atomic_packs_for(&surface, None, None);
    Ok(json!({
        "capability": surface,
        "atomic_packs": packs,
    }))
}