use clap_noun_verb::{NounVerbError, Result};
use clap_noun_verb_macros::verb;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use ggen_core::agent::{emit_install_receipt, PackInstallClosure};
use ggen_core::domain::packs::metadata::show_pack;
use ggen_core::domain::packs::validate::validate_pack;
use ggen_core::packs::lockfile::{LockedPack, PackLockfile, PackSource};
fn project_root() -> Result<PathBuf> {
std::env::current_dir()
.map_err(|e| NounVerbError::execution_error(format!("cannot resolve project dir: {}", e)))
}
fn lockfile_path(root: &Path) -> PathBuf {
root.join(".ggen").join("packs.lock")
}
fn load_or_new_lockfile(path: &Path) -> Result<PackLockfile> {
if path.exists() {
PackLockfile::from_file(path)
.map_err(|e| NounVerbError::execution_error(format!("cannot read lockfile: {}", e)))
} else {
Ok(PackLockfile::new(env!("CARGO_PKG_VERSION")))
}
}
fn validate_pack_id(pack_id: &str) -> Result<()> {
if pack_id.trim().is_empty() {
return Err(NounVerbError::argument_error("pack id must not be empty"));
}
Ok(())
}
#[verb]
pub fn install(pack_id: String) -> Result<Value> {
validate_pack_id(&pack_id)?;
let root = project_root()?;
let lock_path = lockfile_path(&root);
let (version, status) = match show_pack(&pack_id) {
Ok(p) => (p.version, "installed"),
Err(_) => ("0.0.0".to_string(), "declared"),
};
let digest = ggen_core::calculate_sha256(format!("{}@{}", pack_id, version).as_bytes());
let mut lockfile = load_or_new_lockfile(&lock_path)?;
lockfile.add_pack(
&pack_id,
LockedPack {
version: version.clone(),
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)))?;
let artifacts = vec![lock_path.clone()];
let no_packages: Vec<String> = Vec::new();
let closure = PackInstallClosure {
pack_id: &pack_id,
pack_version: &version,
pack_digest: &digest,
packages_installed: &no_packages,
artifact_paths: &artifacts,
};
let receipt = emit_install_receipt(&root, &closure)
.map_err(|e| NounVerbError::execution_error(format!("receipt emission failed: {}", e)))?;
Ok(json!({
"pack_id": pack_id,
"status": status,
"version": version,
"digest": digest,
"lockfile": lock_path.display().to_string(),
"receipt": receipt.display().to_string(),
}))
}
#[verb]
pub fn list() -> Result<Value> {
let root = project_root()?;
let lock_path = lockfile_path(&root);
let packs: Vec<Value> = if lock_path.exists() {
let lf = load_or_new_lockfile(&lock_path)?;
lf.packs
.iter()
.map(|(id, locked)| {
json!({
"pack_id": id,
"version": locked.version,
"integrity": locked.integrity,
})
})
.collect()
} else {
Vec::new()
};
Ok(json!({ "total": packs.len(), "packs": packs }))
}
#[verb]
pub fn validate(pack_id: String) -> Result<Value> {
validate_pack_id(&pack_id)?;
let (is_valid, score, errors) = match validate_pack(&pack_id) {
Ok(r) => (r.valid, r.score, r.errors),
Err(e) => (false, 0.0, vec![e.to_string()]),
};
Ok(json!({
"pack_id": pack_id,
"is_valid": is_valid,
"score": score,
"errors": errors,
}))
}
#[verb]
pub fn show(pack_id: String) -> Result<Value> {
validate_pack_id(&pack_id)?;
match show_pack(&pack_id) {
Ok(p) => Ok(json!({
"pack_id": p.id,
"found": true,
"name": p.name,
"version": p.version,
"description": p.description,
"packages": p.packages,
})),
Err(e) => Ok(json!({
"pack_id": pack_id,
"found": false,
"message": format!("pack not found: {}", e),
})),
}
}