feat(cli): add pic command-line client (login, apps, scripts, logs)

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>
This commit is contained in:
MechaCat02
2026-05-28 20:53:49 +02:00
parent b42e273479
commit 7b50047730
13 changed files with 1448 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
//! `pic apps ls` and `pic apps create`.
use anyhow::Result;
use crate::client::{Client, CreateAppBody};
use crate::config::load;
use crate::output::Table;
pub async fn ls() -> Result<()> {
let creds = load()?;
let client = Client::from_creds(&creds)?;
let apps = client.apps_list().await?;
let mut table = Table::new(["slug", "name", "my_role", "created_at"]);
for app in apps {
// The list endpoint returns App without my_role. We do a per-app
// lookup only on demand; for `ls` we leave the column dashed so
// the call stays cheap (one HTTP request).
table.row([
app.slug.clone(),
app.name.clone(),
"-".to_string(),
app.created_at.to_rfc3339(),
]);
}
table.print();
Ok(())
}
pub async fn create(slug: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
let creds = load()?;
let client = Client::from_creds(&creds)?;
let body = CreateAppBody {
slug,
name: name.unwrap_or(slug),
description,
};
let app = client.apps_create(&body).await?;
println!("Created app {}", app.slug);
Ok(())
}