//! `pic logs ` — print recent execution log rows. //! //! In TSV mode emits a header + truncated-summary rows (`pic logs` was //! previously headerless — inconsistent with `apps ls` / `scripts ls`). //! In JSON mode emits the raw `ExecutionLog` array (no truncation), //! letting `jq` consumers see request/response bodies in full. use anyhow::Result; use picloud_shared::{ExecutionLog, ExecutionStatus}; use crate::client::Client; use crate::config; use crate::output::{OutputMode, Table}; pub async fn run(script_id: &str, limit: u32, mode: OutputMode) -> Result<()> { let creds = config::resolve()?; let client = Client::from_creds(&creds)?; let entries = client.logs_list(script_id, limit).await?; match mode { OutputMode::Tsv => render_tsv(&entries), OutputMode::Json => render_json(&entries), } Ok(()) } fn render_tsv(entries: &[ExecutionLog]) { let mut table = Table::new(["created_at", "status", "summary"]); for e in entries { let summary = summarize(&e.response_body, &e.script_logs); table.row([ e.created_at.to_rfc3339(), status_label(&e.status).to_string(), truncate(&summary, 120), ]); } table.print(OutputMode::Tsv); } fn render_json(entries: &[ExecutionLog]) { // Pretty for human jq-piping; consumers that want compact can pipe // through `jq -c`. let s = serde_json::to_string_pretty(entries).unwrap_or_else(|_| "[]".to_string()); println!("{s}"); } fn status_label(s: &ExecutionStatus) -> &'static str { match s { ExecutionStatus::Success => "success", ExecutionStatus::Error => "error", ExecutionStatus::Timeout => "timeout", ExecutionStatus::BudgetExceeded => "budget_exceeded", } } fn summarize(response_body: &Option, script_logs: &serde_json::Value) -> String { // Prefer the last script-side log line (often the most useful for // grepping). Fall back to the response body. if let Some(arr) = script_logs.as_array() { if let Some(last) = arr.last() { if let Some(msg) = last.get("message").and_then(|m| m.as_str()) { return msg.to_string(); } } } response_body .as_ref() .map(ToString::to_string) .unwrap_or_else(|| "-".to_string()) } fn truncate(s: &str, n: usize) -> String { let normalized = s.replace('\n', " "); if normalized.chars().count() <= n { normalized } else { let head: String = normalized.chars().take(n).collect(); format!("{head}…") } }