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:
@@ -127,6 +127,15 @@ fn render_tree(plan: &TreePlanDto, mode: OutputMode) {
|
||||
]);
|
||||
}
|
||||
}
|
||||
for w in &n.warnings {
|
||||
table.row([
|
||||
node.clone(),
|
||||
"warning".to_string(),
|
||||
String::new(),
|
||||
w.clone(),
|
||||
String::new(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
table.print(mode);
|
||||
render_approvals(&plan.approvals_required, mode);
|
||||
@@ -143,6 +152,18 @@ fn render_approvals(approvals_required: &[String], mode: OutputMode) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Surface the desired-state warnings `apply` would emit, so `pic plan` is a
|
||||
/// faithful preview for CI (disabled binding, unreachable endpoint, dangling
|
||||
/// suppress). JSON callers read them off the `warnings` field.
|
||||
fn render_warnings(warnings: &[String], mode: OutputMode) {
|
||||
if mode != OutputMode::Json && !warnings.is_empty() {
|
||||
eprintln!("\nwarnings:");
|
||||
for w in warnings {
|
||||
eprintln!(" - {w}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assemble the wire bundle: scripts carry inlined source (read from
|
||||
/// their `file`), routes pass through, triggers flatten into a tagged
|
||||
/// array, secrets are names only.
|
||||
@@ -277,4 +298,5 @@ fn render(plan: &PlanDto, mode: OutputMode) {
|
||||
}
|
||||
table.print(mode);
|
||||
render_approvals(&plan.approvals_required, mode);
|
||||
render_warnings(&plan.warnings, mode);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user