//! `pic config --effective` — read-only view of an app's resolved //! configuration, with secret values masked (§4.6). //! //! Two sections: //! * `vars` — the resolved (group-inherited) config vars, each `key = value` //! annotated with the owner that won (kind + depth) and its scope. Pass //! `--explain` to also dump each var's `merged_from` provenance — the //! ordered (depth, scope) layers that fed the resolution. //! * `secrets` — masked statuses, cross-referenced against the manifest so an //! operator can see which declared secrets are still unset and which live //! secrets aren't declared. Values are never fetched or shown here; the //! server reports only `` / `` plus the owning layer. //! //! The vars + masked-secret owner info comes from the `/config/effective` //! endpoint; the manifest is only used to flag `declared`/`unset` drift. use std::collections::BTreeSet; use std::path::Path; use anyhow::{bail, Result}; use crate::client::{Client, EffectiveOwnerDto}; use crate::config; use crate::manifest::Manifest; use crate::output::{OutputMode, Table}; /// Render an owner as `kind@depth` (e.g. `group@1`, `app@0`). fn owner_label(owner: &EffectiveOwnerDto) -> String { format!("{}@{}", owner.kind, owner.depth) } pub async fn run( manifest_path: &Path, effective: bool, explain: bool, env: Option<&str>, mode: OutputMode, ) -> Result<()> { if !effective { bail!("`pic config` currently supports only `--effective`"); } let creds = config::resolve()?; let client = Client::from_creds(&creds)?; // Resolve against the same env overlay as `pic plan`/`apply` so the // masked report targets the app those commands would act on — not the // base slug — when an overlay re-points slug/secrets. let manifest = Manifest::load_with_env(manifest_path, env)?; if manifest.is_group() { bail!( "`pic config --effective` resolves an APP's inherited config; \ a [group] manifest has no single effective view" ); } let eff = client.config_effective(manifest.slug()).await?; // --- vars: the resolved view, with winning owner + provenance. --- let mut vars_table = Table::new(["key", "value", "owner", "scope"]); for (key, var) in &eff.vars { vars_table.row([ key.clone(), var.value.to_string(), owner_label(&var.owner), var.scope.clone(), ]); } vars_table.print(mode); if explain { // Provenance: the (depth, scope) layers each resolved key merged from. let mut prov = Table::new(["key", "depth", "scope"]); for (key, var) in &eff.vars { for layer in &var.merged_from { prov.row([key.clone(), layer.depth.to_string(), layer.scope.clone()]); } } prov.print(mode); } // --- secrets: masked status, folding manifest drift + owning layer. --- let declared: BTreeSet = manifest.secrets.names.iter().cloned().collect(); let on_server: BTreeSet = eff.secrets.keys().cloned().collect(); let mut table = Table::new(["secret", "value", "status", "owner", "scope"]); for name in declared.union(&on_server) { let (value, status) = match (declared.contains(name), on_server.contains(name)) { (true, true) => ("", "managed"), (true, false) => ("", "declared, not pushed — `pic secrets set`"), (false, true) => ("", "on server, not in manifest"), (false, false) => unreachable!("name came from one of the two sets"), }; let (owner, scope) = match eff.secrets.get(name) { Some(s) => (owner_label(&s.owner), s.scope.clone()), None => ("-".to_string(), "-".to_string()), }; table.row([ name.clone(), value.to_string(), status.to_string(), owner, scope, ]); } table.print(mode); Ok(()) }