feat(cli): plan warnings, apply confirm + blast-radius gate, files --group, unified owner-XOR

Remediate the CLI/DX findings from the 2026-07-11 audit.

B4 — `pic plan` now previews the apply-time desired-state warnings (disabled
binding, unreachable endpoint, dangling `[suppress]`), so plan and apply agree
for CI review. Wire fields are `#[serde(default)]` (older-server tolerant).

B5 — `pic apply` no longer mutates on a first run (no recorded plan) without a
preview + confirm, and a large blast radius now triggers an extra confirmation.
Both gates check `is_terminal()` before any read — a non-TTY never hangs on
stdin and fails closed without `--yes`; `--yes`/`--force` bypass headlessly.
`--force` help now notes it also skips these prompts.

C3 — `pic files ls`/`get` gain `--group`, mirroring `pic kv --group`, so the
shipped group shared-files admin surface is reachable from the CLI.

C4 — a shared `require_one_owner(app, group)` helper (cmds/mod.rs) gives one
canonical message for the `--app`/`--group` XOR, wired into every such site
(kv, files, vars, secrets, suppress, extension-points, triggers ls, scripts
deploy). routes ls (positional script_id) and scripts ls (lists all) keep their
own shapes deliberately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 23:47:59 +02:00
parent 5e1bda1f32
commit a3c4267f6c
12 changed files with 289 additions and 52 deletions

View File

@@ -15,14 +15,13 @@ use crate::client::{Client, VarOwnerArg};
use crate::config;
use crate::output::{OutputMode, Table};
/// Resolve the `--group`/`--app` pair into exactly one owner.
/// 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<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")),
}
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(