Skip to main content

ggen_cli_lib/cmds/
capability.rs

1//! Capability noun — resolve and enable capability surfaces
2//! (`ggen capability <verb>`).
3//!
4//! A capability (e.g. `mcp`, `web`, `devops`) expands to a set of *atomic packs*.
5//! `enable` records those packs in the project lockfile so a subsequent
6//! `ggen sync` can generate from them; `list` and `inspect` are read-only
7//! discovery. This is the capability-oriented entry to the same lockfile the
8//! `packs` noun manages, and a natural first step for an agent bringing a project
9//! up: pick a capability, enable it, then sync.
10
11use clap_noun_verb::{NounVerbError, Result};
12use clap_noun_verb_macros::verb;
13use serde_json::{json, Value};
14use std::path::PathBuf;
15
16use ggen_core::domain::packs::capability_registry::{
17    list_capabilities, resolve_capability_to_packs,
18};
19use ggen_core::packs::lockfile::{LockedPack, PackLockfile, PackSource};
20
21// ── helpers ─────────────────────────────────────────────────────────────────
22
23fn project_root() -> Result<PathBuf> {
24    std::env::current_dir()
25        .map_err(|e| NounVerbError::execution_error(format!("cannot resolve project dir: {}", e)))
26}
27
28/// Resolve the atomic packs for a capability surface, appending a
29/// `projection-<proj>` pack when `--projection` is supplied.
30fn atomic_packs_for(surface: &str, projection: Option<&str>, runtime: Option<&str>) -> Vec<String> {
31    let mut packs = resolve_capability_to_packs(surface, projection, runtime).unwrap_or_default();
32    if let Some(p) = projection {
33        let projection_pack = format!("projection-{}", p);
34        if !packs.contains(&projection_pack) {
35            packs.push(projection_pack);
36        }
37    }
38    packs
39}
40
41fn require_surface(surface: &str) -> Result<()> {
42    if surface.trim().is_empty() {
43        return Err(NounVerbError::argument_error(
44            "capability surface must not be empty",
45        ));
46    }
47    Ok(())
48}
49
50// ── verbs ───────────────────────────────────────────────────────────────────
51
52/// Enable a capability: expand it to atomic packs and record them in the project
53/// lockfile, returning the expansion as JSON.
54#[verb]
55pub fn enable(
56    #[arg(index = 1)] surface: String, projection: Option<String>, runtime: Option<String>,
57) -> Result<Value> {
58    require_surface(&surface)?;
59    let packs = atomic_packs_for(&surface, projection.as_deref(), runtime.as_deref());
60
61    // Record each atomic pack as a declared lockfile entry.
62    let root = project_root()?;
63    let lock_path = root.join(".ggen").join("packs.lock");
64    let mut lockfile = if lock_path.exists() {
65        PackLockfile::from_file(&lock_path)
66            .map_err(|e| NounVerbError::execution_error(format!("cannot read lockfile: {}", e)))?
67    } else {
68        PackLockfile::new(env!("CARGO_PKG_VERSION"))
69    };
70    for pid in &packs {
71        let digest = ggen_core::calculate_sha256(format!("{}@0.0.0", pid).as_bytes());
72        lockfile.add_pack(
73            pid,
74            LockedPack {
75                version: "0.0.0".to_string(),
76                source: PackSource::Registry {
77                    url: "https://registry.ggen.io".to_string(),
78                },
79                integrity: Some(format!("sha256-{}", digest)),
80                installed_at: chrono::Utc::now(),
81                dependencies: Vec::new(),
82            },
83        );
84    }
85    lockfile
86        .save(&lock_path)
87        .map_err(|e| NounVerbError::execution_error(format!("cannot write lockfile: {}", e)))?;
88
89    Ok(json!({
90        "capability": surface,
91        "projection": projection,
92        "runtime": runtime,
93        "atomic_packs": packs,
94        "lockfile": lock_path.display().to_string(),
95    }))
96}
97
98/// List the known capability surfaces.
99#[verb]
100pub fn list() -> Result<Value> {
101    let caps: Vec<Value> = list_capabilities()
102        .into_iter()
103        .map(|c| {
104            json!({
105                "id": c.id,
106                "name": c.name,
107                "description": c.description,
108                "category": c.category,
109                "atomic_packs": c.atomic_packs,
110            })
111        })
112        .collect();
113    Ok(json!({ "total": caps.len(), "capabilities": caps }))
114}
115
116/// Inspect a capability surface: show the atomic packs it expands to.
117#[verb]
118pub fn inspect(#[arg(index = 1)] surface: String) -> Result<Value> {
119    require_surface(&surface)?;
120    let packs = atomic_packs_for(&surface, None, None);
121    Ok(json!({
122        "capability": surface,
123        "atomic_packs": packs,
124    }))
125}