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>
61 lines
1.9 KiB
Rust
61 lines
1.9 KiB
Rust
//! `pic kv ls | get` — read-only KV inspection (G2).
|
|
//!
|
|
//! Wraps the read-only `/api/v1/admin/apps/{id}/kv*` surface. There is no
|
|
//! `set`/`rm` on purpose: KV writes go through `kv::set` in scripts (which
|
|
//! emit the change events triggers depend on); an admin write would bypass
|
|
//! that, so the CLI stays read-only (matching the queues precedent).
|
|
|
|
use std::io::Write;
|
|
|
|
use anyhow::Result;
|
|
|
|
use crate::client::Client;
|
|
use crate::config;
|
|
use crate::output::{OutputMode, Table};
|
|
|
|
pub async fn ls(
|
|
app: Option<&str>,
|
|
group: Option<&str>,
|
|
collection: &str,
|
|
limit: u32,
|
|
mode: OutputMode,
|
|
) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
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.
|
|
crate::cmds::OwnerRef::Group(g) => client.group_kv_list(g, collection, limit).await?,
|
|
};
|
|
let mut table = Table::new(["key"]);
|
|
for k in page.keys {
|
|
table.row([k]);
|
|
}
|
|
table.print(mode);
|
|
if page.next_cursor.is_some() {
|
|
let _ = writeln!(
|
|
std::io::stderr(),
|
|
"(more keys available — raise --limit to see them)"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get(
|
|
app: Option<&str>,
|
|
group: Option<&str>,
|
|
collection: &str,
|
|
key: &str,
|
|
) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
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());
|
|
println!("{pretty}");
|
|
Ok(())
|
|
}
|