ggen_cli_lib/cmds/
packs.rs1use clap_noun_verb::{NounVerbError, Result};
14use clap_noun_verb_macros::verb;
15use serde_json::{json, Value};
16use std::path::{Path, PathBuf};
17
18use ggen_core::agent::{emit_install_receipt, PackInstallClosure};
19use ggen_core::domain::packs::metadata::show_pack;
20use ggen_core::domain::packs::validate::validate_pack;
21use ggen_core::packs::lockfile::{LockedPack, PackLockfile, PackSource};
22
23fn project_root() -> Result<PathBuf> {
26 std::env::current_dir()
27 .map_err(|e| NounVerbError::execution_error(format!("cannot resolve project dir: {}", e)))
28}
29
30fn lockfile_path(root: &Path) -> PathBuf {
31 root.join(".ggen").join("packs.lock")
32}
33
34fn load_or_new_lockfile(path: &Path) -> Result<PackLockfile> {
35 if path.exists() {
36 PackLockfile::from_file(path)
37 .map_err(|e| NounVerbError::execution_error(format!("cannot read lockfile: {}", e)))
38 } else {
39 Ok(PackLockfile::new(env!("CARGO_PKG_VERSION")))
40 }
41}
42
43fn validate_pack_id(pack_id: &str) -> Result<()> {
44 if pack_id.trim().is_empty() {
45 return Err(NounVerbError::argument_error("pack id must not be empty"));
46 }
47 Ok(())
48}
49
50#[verb]
61pub fn install(pack_id: String) -> Result<Value> {
62 validate_pack_id(&pack_id)?;
63 let root = project_root()?;
64 let lock_path = lockfile_path(&root);
65
66 let (version, status) = match show_pack(&pack_id) {
69 Ok(p) => (p.version, "installed"),
70 Err(_) => ("0.0.0".to_string(), "declared"),
71 };
72
73 let digest = ggen_core::calculate_sha256(format!("{}@{}", pack_id, version).as_bytes());
75
76 let mut lockfile = load_or_new_lockfile(&lock_path)?;
77 lockfile.add_pack(
78 &pack_id,
79 LockedPack {
80 version: version.clone(),
81 source: PackSource::Registry {
82 url: "https://registry.ggen.io".to_string(),
83 },
84 integrity: Some(format!("sha256-{}", digest)),
85 installed_at: chrono::Utc::now(),
86 dependencies: Vec::new(),
87 },
88 );
89 lockfile
90 .save(&lock_path)
91 .map_err(|e| NounVerbError::execution_error(format!("cannot write lockfile: {}", e)))?;
92
93 let artifacts = vec![lock_path.clone()];
95 let no_packages: Vec<String> = Vec::new();
96 let closure = PackInstallClosure {
97 pack_id: &pack_id,
98 pack_version: &version,
99 pack_digest: &digest,
100 packages_installed: &no_packages,
101 artifact_paths: &artifacts,
102 };
103 let receipt = emit_install_receipt(&root, &closure)
104 .map_err(|e| NounVerbError::execution_error(format!("receipt emission failed: {}", e)))?;
105
106 Ok(json!({
107 "pack_id": pack_id,
108 "status": status,
109 "version": version,
110 "digest": digest,
111 "lockfile": lock_path.display().to_string(),
112 "receipt": receipt.display().to_string(),
113 }))
114}
115
116#[verb]
118pub fn list() -> Result<Value> {
119 let root = project_root()?;
120 let lock_path = lockfile_path(&root);
121 let packs: Vec<Value> = if lock_path.exists() {
122 let lf = load_or_new_lockfile(&lock_path)?;
123 lf.packs
124 .iter()
125 .map(|(id, locked)| {
126 json!({
127 "pack_id": id,
128 "version": locked.version,
129 "integrity": locked.integrity,
130 })
131 })
132 .collect()
133 } else {
134 Vec::new()
135 };
136 Ok(json!({ "total": packs.len(), "packs": packs }))
137}
138
139#[verb]
142pub fn validate(pack_id: String) -> Result<Value> {
143 validate_pack_id(&pack_id)?;
144 let (is_valid, score, errors) = match validate_pack(&pack_id) {
145 Ok(r) => (r.valid, r.score, r.errors),
146 Err(e) => (false, 0.0, vec![e.to_string()]),
147 };
148 Ok(json!({
149 "pack_id": pack_id,
150 "is_valid": is_valid,
151 "score": score,
152 "errors": errors,
153 }))
154}
155
156#[verb]
158pub fn show(pack_id: String) -> Result<Value> {
159 validate_pack_id(&pack_id)?;
160 match show_pack(&pack_id) {
161 Ok(p) => Ok(json!({
162 "pack_id": p.id,
163 "found": true,
164 "name": p.name,
165 "version": p.version,
166 "description": p.description,
167 "packages": p.packages,
168 })),
169 Err(e) => Ok(json!({
170 "pack_id": pack_id,
171 "found": false,
172 "message": format!("pack not found: {}", e),
173 })),
174 }
175}