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:
@@ -267,6 +267,10 @@ struct ConfigArgs {
|
||||
/// Show the effective (resolved) config with secrets masked.
|
||||
#[arg(long)]
|
||||
effective: bool,
|
||||
/// With `--effective`, also print each resolved var's `merged_from`
|
||||
/// provenance — the ordered (depth, scope) layers it merged from.
|
||||
#[arg(long)]
|
||||
explain: bool,
|
||||
/// Resolve against the `picloud.<env>.toml` overlay (per-env slug +
|
||||
/// secrets), matching `pic plan --env` / `pic apply --env`.
|
||||
#[arg(long)]
|
||||
@@ -1142,31 +1146,67 @@ enum DeadLettersCmd {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SecretsCmd {
|
||||
/// List secret names + last-modified for an app. Values never
|
||||
/// leave the server.
|
||||
/// List secret names + last-modified for the owner. Values never
|
||||
/// leave the server. `--env` filters group secrets (app secrets are
|
||||
/// env-agnostic; the `env` column shows the scope for groups).
|
||||
Ls {
|
||||
/// Owning group (slug or id). Mutually exclusive with `--app`.
|
||||
#[arg(long)]
|
||||
app: String,
|
||||
group: Option<String>,
|
||||
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
/// Environment scope (group owners only).
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
},
|
||||
|
||||
/// Set a secret. Reads the value from stdin (the only safe
|
||||
/// channel — inline values would land in shell history). Pipe
|
||||
/// the value in: `echo -n "mysecret" | pic secrets set --app foo my_key`.
|
||||
/// For group owners, `--env` scopes the secret.
|
||||
Set {
|
||||
/// Owning group (slug or id). Mutually exclusive with `--app`.
|
||||
#[arg(long)]
|
||||
app: String,
|
||||
group: Option<String>,
|
||||
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
name: String,
|
||||
/// Environment scope (group owners only).
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
/// Treat stdin as raw JSON (numbers, maps, …) instead of a
|
||||
/// string literal.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
|
||||
/// Delete a secret by name.
|
||||
/// Delete a secret by name (optionally env-scoped for groups).
|
||||
Rm {
|
||||
/// Owning group (slug or id). Mutually exclusive with `--app`.
|
||||
#[arg(long)]
|
||||
app: String,
|
||||
group: Option<String>,
|
||||
/// Owning app (slug or id). Mutually exclusive with `--group`.
|
||||
#[arg(long)]
|
||||
app: Option<String>,
|
||||
name: String,
|
||||
/// Environment scope (group owners only).
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
},
|
||||
|
||||
/// 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 — hence `--group` only, no `--app`.
|
||||
Read {
|
||||
/// Owning group (slug or id).
|
||||
#[arg(long)]
|
||||
group: String,
|
||||
name: String,
|
||||
/// Environment scope.
|
||||
#[arg(long)]
|
||||
env: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1293,7 +1333,14 @@ async fn main() -> ExitCode {
|
||||
Cmd::Plan(args) => cmds::plan::run(&args.file, args.env.as_deref(), mode).await,
|
||||
Cmd::Pull(args) => cmds::pull::run(&args.app, &args.dir, args.force, mode).await,
|
||||
Cmd::Config(args) => {
|
||||
cmds::config::run(&args.file, args.effective, args.env.as_deref(), mode).await
|
||||
cmds::config::run(
|
||||
&args.file,
|
||||
args.effective,
|
||||
args.explain,
|
||||
args.env.as_deref(),
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Cmd::Init(args) => cmds::init::run(
|
||||
&args.dir,
|
||||
@@ -1796,14 +1843,39 @@ async fn main() -> ExitCode {
|
||||
cmd: DeadLettersCmd::Resolve { app, dl_id, reason },
|
||||
} => cmds::dead_letters::resolve(&app, &dl_id, &reason).await,
|
||||
Cmd::Secrets {
|
||||
cmd: SecretsCmd::Ls { app },
|
||||
} => cmds::secrets::ls(&app, mode).await,
|
||||
cmd: SecretsCmd::Ls { group, app, env },
|
||||
} => cmds::secrets::ls(group.as_deref(), app.as_deref(), env.as_deref(), mode).await,
|
||||
Cmd::Secrets {
|
||||
cmd: SecretsCmd::Set { app, name, json },
|
||||
} => cmds::secrets::set(&app, &name, json).await,
|
||||
cmd:
|
||||
SecretsCmd::Set {
|
||||
group,
|
||||
app,
|
||||
name,
|
||||
env,
|
||||
json,
|
||||
},
|
||||
} => {
|
||||
cmds::secrets::set(
|
||||
group.as_deref(),
|
||||
app.as_deref(),
|
||||
&name,
|
||||
env.as_deref(),
|
||||
json,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Cmd::Secrets {
|
||||
cmd: SecretsCmd::Rm { app, name },
|
||||
} => cmds::secrets::rm(&app, &name).await,
|
||||
cmd:
|
||||
SecretsCmd::Rm {
|
||||
group,
|
||||
app,
|
||||
name,
|
||||
env,
|
||||
},
|
||||
} => cmds::secrets::rm(group.as_deref(), app.as_deref(), &name, env.as_deref()).await,
|
||||
Cmd::Secrets {
|
||||
cmd: SecretsCmd::Read { group, name, env },
|
||||
} => cmds::secrets::read(&group, &name, env.as_deref()).await,
|
||||
Cmd::Members {
|
||||
cmd: MembersCmd::Ls { app },
|
||||
} => cmds::members::ls(&app, mode).await,
|
||||
|
||||
Reference in New Issue
Block a user