A CLI-only walkthrough (E2E_TODO_REPORT.md) found two control-plane
operations with no `pic` command — every CLI-created app 404'd until it
claimed a domain, and external SSE feeds needed a raw topic-registration
call — plus several friction points. All admin APIs already existed; this
adds thin wrappers mirroring `routes`/`triggers`:
- B1: `pic apps domains {ls,add,rm}` over apps/{id}/domains.
- B2: `pic topics {ls,create,update,rm}` over apps/{id}/topics, and a
triggers help note distinguishing a pubsub trigger from topic
registration.
- F2: `pic users {ls,show,reset-password,revoke-sessions}` over the app
end-user admin surface (read + the two admin actions; create/invitations
deferred).
- F1: `apps create` and `deploy` now honor `--output json`, emitting the
created object so scripts can capture the id.
- F3: non-interactive login via `--username` + `--password-stdin` (inline
passwords still rejected, mirroring the `--token` rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.1 KiB
Rust
63 lines
2.1 KiB
Rust
//! `pic apps domains {ls,add,rm}` — manage an app's domain claims.
|
|
//!
|
|
//! Host→app dispatch resolves an incoming `Host` to the app that claims
|
|
//! it; a non-default app's routes are unreachable until it claims a
|
|
//! domain. This is the CLI surface over the `apps/{id}/domains` admin API.
|
|
//!
|
|
//! Param-syntax note: domain patterns use `{name}` for the parameterized
|
|
//! wildcard (e.g. `{tenant}.example.com`) — never `:name`, which is
|
|
//! route-path syntax.
|
|
|
|
use anyhow::Result;
|
|
|
|
use crate::client::Client;
|
|
use crate::config;
|
|
use crate::output::{KvBlock, OutputMode, Table};
|
|
|
|
pub async fn ls(app: &str, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let domains = client.domains_list(app).await?;
|
|
let mut table = Table::new(["id", "pattern", "shape", "created_at"]);
|
|
for d in domains {
|
|
table.row([
|
|
d.id.to_string(),
|
|
d.pattern.clone(),
|
|
shape_label(d.shape).to_string(),
|
|
d.created_at.to_rfc3339(),
|
|
]);
|
|
}
|
|
table.print(mode);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn add(app: &str, pattern: &str, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let d = client.domains_create(app, pattern).await?;
|
|
let mut block = KvBlock::new();
|
|
block
|
|
.field("id", d.id.to_string())
|
|
.field("pattern", d.pattern.clone())
|
|
.field("shape", shape_label(d.shape).to_string())
|
|
.field("created_at", d.created_at.to_rfc3339());
|
|
block.print(mode);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn rm(app: &str, domain_id: &str) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
client.domains_delete(app, domain_id).await?;
|
|
println!("Deleted domain {domain_id}");
|
|
Ok(())
|
|
}
|
|
|
|
fn shape_label(shape: picloud_shared::DomainShape) -> &'static str {
|
|
match shape {
|
|
picloud_shared::DomainShape::Exact => "exact",
|
|
picloud_shared::DomainShape::Wildcard => "wildcard",
|
|
picloud_shared::DomainShape::Parameterized => "parameterized",
|
|
}
|
|
}
|