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:
@@ -1139,6 +1139,50 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// §11.6 M4: a group's shared-files collection (read-only admin surface).
|
||||
/// `GET /api/v1/admin/groups/{id_or_slug}/files?collection=&limit=`
|
||||
pub async fn group_files_list(
|
||||
&self,
|
||||
group: &str,
|
||||
collection: &str,
|
||||
limit: u32,
|
||||
) -> Result<ListFilesResponse> {
|
||||
let (group, collection) = (seg(group), seg(collection));
|
||||
let resp = self
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"/api/v1/admin/groups/{group}/files?collection={collection}&limit={limit}"
|
||||
),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/groups/{id_or_slug}/files/{collection}/{file_id}` —
|
||||
/// streams a shared file's raw bytes (download).
|
||||
pub async fn group_files_get_bytes(
|
||||
&self,
|
||||
group: &str,
|
||||
collection: &str,
|
||||
file_id: &str,
|
||||
) -> Result<Vec<u8>> {
|
||||
let (group, collection, file_id) = (seg(group), seg(collection), seg(file_id));
|
||||
let resp = self
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!("/api/v1/admin/groups/{group}/files/{collection}/{file_id}"),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status().is_success() {
|
||||
Ok(resp.bytes().await.context("reading file bytes")?.to_vec())
|
||||
} else {
|
||||
Err(server_error(resp).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// `DELETE /api/v1/admin/apps/{id_or_slug}/files/{collection}/{file_id}`
|
||||
pub async fn files_delete(&self, app: &str, collection: &str, file_id: &str) -> Result<()> {
|
||||
let (app, collection, file_id) = (seg(app), seg(collection), seg(file_id));
|
||||
@@ -1585,6 +1629,10 @@ pub struct PlanDto {
|
||||
/// §3 M3: envs the governing project marks confirm-required (need `--approve`).
|
||||
#[serde(default)]
|
||||
pub approvals_required: Vec<String>,
|
||||
/// Desired-state warnings `apply` would emit, previewed at plan (disabled
|
||||
/// binding, unreachable endpoint, dangling suppress).
|
||||
#[serde(default)]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -1653,6 +1701,9 @@ pub struct NodePlanDto {
|
||||
/// names the server + manifest parents).
|
||||
#[serde(default)]
|
||||
pub structure: Option<StructurePreviewDto>,
|
||||
/// Desired-state warnings this node's apply would emit, previewed at plan.
|
||||
#[serde(default)]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user