feat(cli): pic secrets --group, value read, and effective vars/config
Mirrors `pic vars` on the secrets surface: `pic secrets ls/set/rm` take an `--app`/`--group` owner selector (exactly-one) with `--env` for group secrets. Adds `pic secrets read --group <g> <name> [--env]` — the only command that reveals a secret value, hitting the group-gated value endpoint. `pic config --effective` now folds in the resolved vars section (value + owner + scope) and an `--explain` provenance view, alongside the existing masked-secrets cross-reference, via `GET /config/effective`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,56 @@
|
||||
//! `pic secrets` subcommands: `ls`, `set`, `rm`.
|
||||
//! `pic secrets ls | set | rm | read` — manage Phase-3 group/app secrets.
|
||||
//!
|
||||
//! 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.
|
||||
//! 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;
|
||||
use crate::client::{Client, VarOwnerArg};
|
||||
use crate::config;
|
||||
use crate::output::{OutputMode, Table};
|
||||
|
||||
pub async fn ls(app: &str, mode: OutputMode) -> Result<()> {
|
||||
/// Resolve the `--group`/`--app` pair into exactly one owner.
|
||||
fn owner<'a>(group: Option<&'a str>, app: Option<&'a str>) -> Result<VarOwnerArg<'a>> {
|
||||
match (group, app) {
|
||||
(Some(g), None) => Ok(VarOwnerArg::Group(g)),
|
||||
(None, Some(a)) => Ok(VarOwnerArg::App(a)),
|
||||
(Some(_), Some(_)) => Err(anyhow!("pass exactly one of --group / --app, not both")),
|
||||
(None, None) => Err(anyhow!("pass one of --group / --app")),
|
||||
}
|
||||
}
|
||||
|
||||
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(app).await?;
|
||||
let mut table = Table::new(["name", "updated_at"]);
|
||||
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.updated_at.to_rfc3339()]);
|
||||
table.row([s.name, s.env, s.updated_at.to_rfc3339()]);
|
||||
}
|
||||
table.print(mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set(app: &str, name: &str, as_json: bool) -> Result<()> {
|
||||
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();
|
||||
@@ -41,15 +66,36 @@ pub async fn set(app: &str, name: &str, as_json: bool) -> Result<()> {
|
||||
} else {
|
||||
serde_json::Value::String(trimmed.to_string())
|
||||
};
|
||||
client.secrets_set(app, name, value).await?;
|
||||
client.secrets_set(owner, name, value, env).await?;
|
||||
println!("Set secret {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rm(app: &str, name: &str) -> Result<()> {
|
||||
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(app, name).await?;
|
||||
client.secrets_delete(owner, name, env).await?;
|
||||
println!("Deleted secret {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `pic secrets read --group <slug> <name>` — 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user