feat(cli): add pic docs/dead-letters ls group read mirrors

Two read-only operator surfaces existed server-side but the CLI was app-only:

- `pic dead-letters ls --group <slug>` — mirrors the app listing onto a group's
  shared-queue dead-letters (§11.6 D3, `GET /groups/{id}/dead-letters`), via the
  existing `require_one_owner`/`OwnerRef` dispatch. List-only (show/replay/
  resolve stay app-only, matching the server's operator surface); a group DL row
  shows its `collection` and carries no trigger, so it renders its own columns.
- `pic docs ls/get --group <slug>` — a new `Docs` command (`cmds/docs.rs`)
  wrapping `GET /groups/{id}/docs[/{collection}/{id}]`, mirroring `pic kv`.
  Group-only: per-app docs have no admin read route yet (unlike kv/files), so
  there is no `--app` variant — noted for a later pass.

Read-only by design (writes go through the SDK). New client methods +
`GroupDocsListDto`/`GroupDeadLetterDto`. Pinned by a new
`operator_reads_shared_docs_and_group_dead_letters_via_cli` journey
(152/152 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-13 20:00:37 +02:00
parent bc2538b5b5
commit 3da70a66c7
6 changed files with 348 additions and 27 deletions

View File

@@ -1318,6 +1318,64 @@ impl Client {
Ok(wrapped.value)
}
/// §11.6: a group's shared-docs collection (operator read).
/// `GET /api/v1/admin/groups/{id_or_slug}/docs?collection=…&limit=…`
pub async fn group_docs_list(
&self,
group: &str,
collection: &str,
limit: u32,
) -> Result<GroupDocsListDto> {
let (group, collection) = (seg(group), seg(collection));
let resp = self
.request(
Method::GET,
&format!("/api/v1/admin/groups/{group}/docs?collection={collection}&limit={limit}"),
)
.send()
.await?;
decode(resp).await
}
/// `GET /api/v1/admin/groups/{id_or_slug}/docs/{collection}/{doc_id}`
pub async fn group_docs_get(
&self,
group: &str,
collection: &str,
doc_id: &str,
) -> Result<Value> {
let (group, collection, doc_id) = (seg(group), seg(collection), seg(doc_id));
let resp = self
.request(
Method::GET,
&format!("/api/v1/admin/groups/{group}/docs/{collection}/{doc_id}"),
)
.send()
.await?;
decode(resp).await
}
/// §11.6 D3: a group's shared-queue dead-letters (operator read; list-only).
/// `GET /api/v1/admin/groups/{id_or_slug}/dead-letters?unresolved=…&limit=…`
pub async fn group_dead_letters_list(
&self,
group: &str,
unresolved: bool,
limit: u32,
) -> Result<Vec<GroupDeadLetterDto>> {
let group = seg(group);
let resp = self
.request(
Method::GET,
&format!(
"/api/v1/admin/groups/{group}/dead-letters?unresolved={unresolved}&limit={limit}"
),
)
.send()
.await?;
decode(resp).await
}
// --- Queues (G2, read-only admin surface) -----------------------------
/// `GET /api/v1/admin/apps/{id_or_slug}/queues`
@@ -2344,6 +2402,37 @@ pub struct KvListPageDto {
pub next_cursor: Option<String>,
}
/// One document in a group's shared-docs collection (operator listing). The
/// server also sends each doc's `data`, but the `ls` view shows ids only (fetch
/// a document's body with `pic docs get`), so it is not deserialized here.
#[derive(Debug, Deserialize)]
pub struct GroupDocEntryDto {
pub id: String,
}
#[derive(Debug, Deserialize)]
pub struct GroupDocsListDto {
pub docs: Vec<GroupDocEntryDto>,
pub next_cursor: Option<String>,
}
/// One group shared-queue dead-letter (operator listing). Distinct from the
/// per-app `DeadLetterDto`: a group DL carries no `app_id`/`trigger_id` and
/// includes the `collection` it belonged to; timestamps are RFC3339 strings.
#[derive(Debug, Deserialize)]
pub struct GroupDeadLetterDto {
pub id: String,
pub collection: String,
pub source: String,
pub op: String,
pub attempt_count: u32,
pub last_error: String,
pub created_at: String,
pub resolved_at: Option<String>,
#[allow(dead_code)]
pub payload: Value,
}
#[derive(Debug, Deserialize)]
struct KvGetResponse {
value: Value,