Adds a new workspace crate `picloud-cli` shipping a `pic` binary that drives the edit-deploy-invoke-tail-logs loop against PiCloud's admin and execute HTTP surface. Eight subcommands cover the minimum a developer needs to never open the dashboard: pic login (paste URL + bearer token, validates via /auth/me) pic whoami (re-validates and prints principal) pic apps ls | create pic scripts ls | deploy | invoke pic logs <id> Credentials persist as TOML under the platform config dir (resolved via `directories`); on POSIX the file is forced to mode 0600. PICLOUD_URL + PICLOUD_TOKEN env vars short-circuit interactive prompts for CI and integration tests. The CLI redeclares minimal request/response structs in `client.rs` rather than depending on `manager-core` — keeps the blast radius contained without touching the existing crate boundaries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
1.7 KiB
Rust
59 lines
1.7 KiB
Rust
//! `pic logs <script-id>` — print recent execution log rows.
|
|
|
|
use anyhow::Result;
|
|
use picloud_shared::ExecutionStatus;
|
|
|
|
use crate::client::Client;
|
|
use crate::config::load;
|
|
|
|
pub async fn run(script_id: &str, limit: u32) -> Result<()> {
|
|
let creds = load()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let entries = client.logs_list(script_id, limit).await?;
|
|
for e in entries {
|
|
let summary = summarize(&e.response_body, &e.script_logs);
|
|
println!(
|
|
"{}\t{}\t{}",
|
|
e.created_at.to_rfc3339(),
|
|
status_label(&e.status),
|
|
truncate(&summary, 120),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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<serde_json::Value>, 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}…")
|
|
}
|
|
}
|