//! `pic secrets ls | set | rm | read` — manage Phase-3 group/app secrets. //! //! Exactly one of `--group` / `--app` selects the owner (mirroring //! `pic vars`). Set reads the secret value from stdin (the only safe //! channel — inline values would leak into shell history). The value is //! sent as a JSON string; pass `--json` to interpret stdin as raw JSON //! (numbers, maps, …) instead. `--env` is only meaningful for group //! owners (app secrets are env-agnostic). use std::io::Read; use anyhow::{anyhow, Context, Result}; use crate::client::{Client, VarOwnerArg}; use crate::config; use crate::output::{OutputMode, Table}; /// Resolve the `--group`/`--app` pair into exactly one owner (shared XOR /// message via [`crate::cmds::require_one_owner`]). fn owner<'a>(group: Option<&'a str>, app: Option<&'a str>) -> Result> { Ok(match crate::cmds::require_one_owner(app, group)? { crate::cmds::OwnerRef::App(a) => VarOwnerArg::App(a), crate::cmds::OwnerRef::Group(g) => VarOwnerArg::Group(g), }) } pub async fn ls( group: Option<&str>, app: Option<&str>, env: Option<&str>, mode: OutputMode, ) -> Result<()> { let owner = owner(group, app)?; let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let resp = client.secrets_list(owner, env).await?; let mut table = Table::new(["name", "env", "updated_at"]); for s in resp.secrets { table.row([s.name, s.env, s.updated_at.to_rfc3339()]); } table.print(mode); Ok(()) } pub async fn set( group: Option<&str>, app: Option<&str>, name: &str, env: Option<&str>, as_json: bool, ) -> Result<()> { let owner = owner(group, app)?; let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let mut buf = String::new(); std::io::stdin() .read_to_string(&mut buf) .context("read secret value from stdin")?; let trimmed = buf.trim_end_matches(['\n', '\r']); if trimmed.is_empty() { return Err(anyhow!("empty stdin — secret value must not be empty")); } let value = if as_json { serde_json::from_str(trimmed).map_err(|e| anyhow!("parse stdin as JSON: {e}"))? } else { serde_json::Value::String(trimmed.to_string()) }; client.secrets_set(owner, name, value, env).await?; println!("Set secret {name}"); Ok(()) } pub async fn rm( group: Option<&str>, app: Option<&str>, name: &str, env: Option<&str>, ) -> Result<()> { let owner = owner(group, app)?; let creds = config::resolve()?; let client = Client::from_creds(&creds)?; client.secrets_delete(owner, name, env).await?; println!("Deleted secret {name}"); Ok(()) } /// `pic secrets read --group ` — fetch and print a secret's /// PLAINTEXT value. This is the ONLY command that reveals a secret value, /// and it is gated server-side at the owning group (there is no app-secret /// equivalent). String values print raw; JSON values print pretty. pub async fn read(group: &str, name: &str, env: Option<&str>) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let resp = client.group_secret_read_value(group, name, env).await?; match resp.value { serde_json::Value::String(s) => println!("{s}"), other => println!("{}", serde_json::to_string_pretty(&other)?), } Ok(()) }