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:
@@ -7,7 +7,7 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::config;
|
||||
@@ -22,11 +22,10 @@ pub async fn ls(
|
||||
) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let page = match (app, group) {
|
||||
(Some(a), None) => client.kv_list(a, collection, limit).await?,
|
||||
let page = match crate::cmds::require_one_owner(app, group)? {
|
||||
crate::cmds::OwnerRef::App(a) => client.kv_list(a, collection, limit).await?,
|
||||
// §11.6 M4: a group's shared-KV collection.
|
||||
(None, Some(g)) => client.group_kv_list(g, collection, limit).await?,
|
||||
_ => bail!("provide exactly one of --app or --group"),
|
||||
crate::cmds::OwnerRef::Group(g) => client.group_kv_list(g, collection, limit).await?,
|
||||
};
|
||||
let mut table = Table::new(["key"]);
|
||||
for k in page.keys {
|
||||
@@ -50,10 +49,9 @@ pub async fn get(
|
||||
) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let value = match (app, group) {
|
||||
(Some(a), None) => client.kv_get(a, collection, key).await?,
|
||||
(None, Some(g)) => client.group_kv_get(g, collection, key).await?,
|
||||
_ => bail!("provide exactly one of --app or --group"),
|
||||
let value = match crate::cmds::require_one_owner(app, group)? {
|
||||
crate::cmds::OwnerRef::App(a) => client.kv_get(a, collection, key).await?,
|
||||
crate::cmds::OwnerRef::Group(g) => client.group_kv_get(g, collection, key).await?,
|
||||
};
|
||||
// Always emit the JSON value (pretty) so it pipes cleanly into jq.
|
||||
let pretty = serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
|
||||
|
||||
Reference in New Issue
Block a user