use clap_noun_verb::Result as VerbResult;
use clap_noun_verb_macros::verb;
use serde::Serialize;
pub use ggen_core::marketplace::policy::{PackContext, PolicyReport};
pub use ggen_core::marketplace::profile::{predefined_profiles, Profile, ProfileId};
#[derive(Serialize)]
pub struct ListOutput {
pub profiles: Vec<ProfileSummary>,
pub total: usize,
}
#[derive(Serialize)]
pub struct ProfileSummary {
pub id: String,
pub name: String,
pub description: String,
pub policy_count: usize,
pub trust_requirement: String,
pub receipt_requirement: String,
}
#[derive(Serialize)]
pub struct ValidateOutput {
pub profile_id: String,
pub passed: bool,
pub violation_count: usize,
pub policies_checked: usize,
pub violations: Vec<ViolationSummary>,
}
#[derive(Serialize)]
pub struct ViolationSummary {
pub policy_id: String,
pub pack_id: String,
pub description: String,
}
#[derive(Serialize)]
pub struct ShowOutput {
pub profile_id: String,
pub name: String,
pub description: String,
pub policies: Vec<PolicySummary>,
pub trust_requirement: String,
pub receipt_requirement: String,
pub runtime_constraints: Vec<RuntimeConstraintSummary>,
}
#[derive(Serialize)]
pub struct PolicySummary {
pub id: String,
pub name: String,
pub description: String,
pub rule_count: usize,
}
#[derive(Serialize)]
pub struct RuntimeConstraintSummary {
pub allowed_runtimes: Vec<String>,
pub forbid_defaults: bool,
pub require_explicit: bool,
}
fn load_pack_contexts_from_project() -> crate::Result<Vec<PackContext>> {
use ggen_core::marketplace::metadata::{get_pack_cache_dir, load_pack_metadata};
use ggen_core::packs::lockfile::PackLockfile;
use std::path::Path;
let lockfile_path = Path::new(".ggen/packs.lock");
if !lockfile_path.exists() {
return Err(ggen_core::utils::error::Error::new(
"No project found. Please install packs first with 'ggen packs install <pack-id>'",
));
}
let lockfile = PackLockfile::from_file(lockfile_path).map_err(|e| {
ggen_core::utils::error::Error::new(&format!("Failed to load lockfile: {}", e))
})?;
let mut pack_contexts = Vec::new();
for (pack_id, locked_pack) in &lockfile.packs {
let package_id = ggen_core::marketplace::models::PackageId::new(pack_id).map_err(|e| {
ggen_core::utils::error::Error::new(&format!("Invalid package ID {}: {}", pack_id, e))
})?;
let cache_dir = get_pack_cache_dir(&package_id, &locked_pack.version);
let metadata = load_pack_metadata(&cache_dir).map_err(|e| {
ggen_core::utils::error::Error::new(&format!(
"Failed to load metadata for pack {}: {}",
pack_id, e
))
})?;
let (template_defaults, runtime) = load_pack_config_from_cache(&cache_dir);
let pack_context = PackContext::new(pack_id.clone())
.with_template_defaults(template_defaults)
.with_signed_receipts(metadata.signature.is_some())
.with_runtime(runtime)
.with_trust_tier(metadata.trust_tier)
.with_signature_verification(metadata.signature.is_some());
pack_contexts.push(pack_context);
}
Ok(pack_contexts)
}
fn load_pack_config_from_cache(cache_dir: &std::path::Path) -> (bool, Option<String>) {
use std::fs;
let pack_toml_path = cache_dir.join("pack.toml");
if !pack_toml_path.exists() {
return (false, None);
}
let content = match fs::read_to_string(&pack_toml_path) {
Ok(c) => c,
Err(_) => return (false, None),
};
let value: toml::Value = match toml::from_str(&content) {
Ok(v) => v,
Err(_) => return (false, None),
};
let template_defaults = value
.get("pack")
.and_then(|p| p.get("use_defaults"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let runtime = value
.get("pack")
.and_then(|p| p.get("runtime"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
(template_defaults, runtime)
}
#[verb]
pub fn list(verbose: bool) -> VerbResult<ListOutput> {
let profiles = predefined_profiles();
if verbose {
log::info!("Available Policy Profiles:");
for profile in &profiles {
log::info!(" - {} ({})", profile.id.as_str(), profile.name);
log::info!(" Description: {}", profile.description);
log::info!(" Policies: {}", profile.policy_overlays.len());
log::info!(" Trust Tier: {:?}", profile.trust_requirements);
log::info!(" Receipt Spec: {:?}", profile.receipt_requirements);
}
}
let profiles_summary: Vec<ProfileSummary> = profiles
.iter()
.map(|p| ProfileSummary {
id: p.id.as_str().to_string(),
name: p.name.clone(),
description: p.description.clone(),
policy_count: p.policy_overlays.len(),
trust_requirement: format!("{:?}", p.trust_requirements),
receipt_requirement: format!("{:?}", p.receipt_requirements),
})
.collect();
Ok(ListOutput {
profiles: profiles_summary,
total: profiles.len(),
})
}
#[verb]
pub fn validate(profile: String) -> VerbResult<ValidateOutput> {
let profile_obj = ggen_core::marketplace::profile::get_profile(&profile).map_err(|e| {
clap_noun_verb::NounVerbError::argument_error(format!("Profile not found: {}", e))
})?;
let pack_contexts = load_pack_contexts_from_project()
.map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
let report = profile_obj.enforce(&pack_contexts).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Policy enforcement failed: {}", e))
})?;
let violations: Vec<ViolationSummary> = report
.violations
.iter()
.map(|v| ViolationSummary {
policy_id: v.policy_id.as_str().to_string(),
pack_id: v.pack_id.clone(),
description: v.description.clone(),
})
.collect();
if report.passed {
log::info!("✓ Profile '{}' validation passed", profile);
} else {
log::error!("✗ Profile '{}' validation failed", profile);
log::error!(" Violations: {}", report.violation_count());
for violation in &violations {
log::error!(" - {}: {}", violation.pack_id, violation.description);
}
}
Ok(ValidateOutput {
profile_id: profile,
passed: report.passed,
violation_count: report.violation_count(),
policies_checked: report.policies_checked.len(),
violations,
})
}
#[verb]
pub fn show(profile_id: String) -> VerbResult<ShowOutput> {
let profile = ggen_core::marketplace::profile::get_profile(&profile_id).map_err(|e| {
clap_noun_verb::NounVerbError::argument_error(format!("Profile not found: {}", e))
})?;
log::info!("Profile: {} ({})", profile.id.as_str(), profile.name);
log::info!("Description: {}", profile.description);
log::info!("Policies ({}):", profile.policy_overlays.len());
for policy in &profile.policy_overlays {
log::info!(" - {} ({})", policy.id.as_str(), policy.name);
log::info!(" {}", policy.description);
log::info!(" Rules: {}", policy.rules.len());
}
log::info!("Trust Tier: {:?}", profile.trust_requirements);
log::info!("Receipt Spec: {:?}", profile.receipt_requirements);
log::info!(
"Runtime Constraints ({}):",
profile.runtime_constraints.len()
);
for (idx, constraint) in profile.runtime_constraints.iter().enumerate() {
log::info!(" Constraint {}:", idx + 1);
log::info!(" Allowed Runtimes: {:?}", constraint.allowed_runtimes);
log::info!(" Forbid Defaults: {}", constraint.forbid_defaults);
log::info!(" Require Explicit: {}", constraint.require_explicit);
}
let policies: Vec<PolicySummary> = profile
.policy_overlays
.iter()
.map(|p| PolicySummary {
id: p.id.as_str().to_string(),
name: p.name.clone(),
description: p.description.clone(),
rule_count: p.rules.len(),
})
.collect();
let runtime_constraints: Vec<RuntimeConstraintSummary> = profile
.runtime_constraints
.iter()
.map(|c| RuntimeConstraintSummary {
allowed_runtimes: c.allowed_runtimes.clone(),
forbid_defaults: c.forbid_defaults,
require_explicit: c.require_explicit,
})
.collect();
Ok(ShowOutput {
profile_id: profile.id.as_str().to_string(),
name: profile.name,
description: profile.description,
policies,
trust_requirement: format!("{:?}", profile.trust_requirements),
receipt_requirement: format!("{:?}", profile.receipt_requirements),
runtime_constraints,
})
}
#[verb]
pub fn check() -> VerbResult<ValidateOutput> {
let profile_obj =
ggen_core::marketplace::profile::get_profile("enterprise-strict").map_err(|e| {
clap_noun_verb::NounVerbError::argument_error(format!(
"Default profile not found: {}",
e
))
})?;
let pack_contexts = load_pack_contexts_from_project()
.map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
let report = profile_obj.enforce(&pack_contexts).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Policy enforcement failed: {}", e))
})?;
let violations: Vec<ViolationSummary> = report
.violations
.iter()
.map(|v| ViolationSummary {
policy_id: v.policy_id.as_str().to_string(),
pack_id: v.pack_id.clone(),
description: v.description.clone(),
})
.collect();
if report.passed {
log::info!(
"✓ Current environment passes '{}' profile",
profile_obj.name
);
} else {
log::error!("✗ Current environment fails '{}' profile", profile_obj.name);
log::error!(" Violations: {}", report.violation_count());
for violation in &violations {
log::error!(" - {}: {}", violation.pack_id, violation.description);
}
}
Ok(ValidateOutput {
profile_id: profile_obj.id.as_str().to_string(),
passed: report.passed,
violation_count: report.violation_count(),
policies_checked: report.policies_checked.len(),
violations,
})
}