feat(cli): pic projects ls + §7 M3/track-complete docs
§7 M3 (part 3) — the ownership track becomes inspectable, completing M1–M3. - server: GET /api/v1/admin/projects (projects_api) lists every project + its owned-group count (projects repo list_with_counts, one GROUP BY); behind the /admin middleware, like the groups list. - CLI: pic projects ls (cmds/projects.rs + client projects_list); the apply_ownership plan-preview journey now also asserts projects ls (plat owns exactly 1 group; teamb listed). - docs: design §7 + CLAUDE.md record M3 shipped and the whole §7 multi-repo ownership track (M1 claim · M2 attach ceiling · M3 preview + projects ls) COMPLETE; deferred = structural-divergence + declarative tree shape + per-env approval gating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,16 @@ impl Client {
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/projects` — every §7 project + its owned-group count.
|
||||
/// Backs `pic projects ls`.
|
||||
pub async fn projects_list(&self) -> Result<Vec<ProjectDto>> {
|
||||
let resp = self
|
||||
.request(Method::GET, "/api/v1/admin/projects")
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/groups/{id_or_slug}` — group + path + children.
|
||||
pub async fn groups_get(&self, ident: &str) -> Result<GroupDetailDto> {
|
||||
let ident = seg(ident);
|
||||
@@ -1708,6 +1718,17 @@ pub struct GroupListItem {
|
||||
pub owner: Option<String>,
|
||||
}
|
||||
|
||||
/// One row of `pic projects ls` (§7 M3): a project + how many group nodes it
|
||||
/// owns.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectDto {
|
||||
pub slug: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owned_groups: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateRouteBody<'a> {
|
||||
pub host_kind: HostKind,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod logout;
|
||||
pub mod logs;
|
||||
pub mod members;
|
||||
pub mod plan;
|
||||
pub mod projects;
|
||||
pub mod pull;
|
||||
pub mod queues;
|
||||
pub mod routes;
|
||||
|
||||
29
crates/picloud-cli/src/cmds/projects.rs
Normal file
29
crates/picloud-cli/src/cmds/projects.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! `pic projects` — read-only view of §7 multi-repo ownership projects.
|
||||
//!
|
||||
//! A project is the repo-root that declaratively owns a slice of the group
|
||||
//! tree. Ownership is CLAIMED by `pic apply` (first apply with a `[project]`
|
||||
//! slug registers it); this command only lists what exists.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::config;
|
||||
use crate::output::{OutputMode, Table};
|
||||
|
||||
/// `pic projects ls` — every project + how many group nodes it owns.
|
||||
pub async fn ls(mode: OutputMode) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let projects = client.projects_list().await?;
|
||||
let mut table = Table::new(["slug", "name", "owned_groups", "created_at"]);
|
||||
for p in &projects {
|
||||
table.row([
|
||||
p.slug.clone(),
|
||||
p.name.clone(),
|
||||
p.owned_groups.to_string(),
|
||||
p.created_at.to_rfc3339(),
|
||||
]);
|
||||
}
|
||||
table.print(mode);
|
||||
Ok(())
|
||||
}
|
||||
@@ -58,6 +58,12 @@ enum Cmd {
|
||||
cmd: GroupsCmd,
|
||||
},
|
||||
|
||||
/// §7 multi-repo ownership projects (read-only).
|
||||
Projects {
|
||||
#[command(subcommand)]
|
||||
cmd: ProjectsCmd,
|
||||
},
|
||||
|
||||
/// Script management.
|
||||
Scripts {
|
||||
#[command(subcommand)]
|
||||
@@ -490,6 +496,12 @@ enum AppsCmd {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ProjectsCmd {
|
||||
/// List all projects + how many group nodes each owns.
|
||||
Ls,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum GroupsCmd {
|
||||
/// List all groups (flat).
|
||||
@@ -1509,6 +1521,9 @@ async fn main() -> ExitCode {
|
||||
cmd: DomainsCmd::Rm { app, domain_id },
|
||||
},
|
||||
} => cmds::apps_domains::rm(&app, &domain_id).await,
|
||||
Cmd::Projects {
|
||||
cmd: ProjectsCmd::Ls,
|
||||
} => cmds::projects::ls(mode).await,
|
||||
Cmd::Groups { cmd: GroupsCmd::Ls } => cmds::groups::ls(mode).await,
|
||||
Cmd::Groups {
|
||||
cmd: GroupsCmd::Tree,
|
||||
|
||||
Reference in New Issue
Block a user