use serde::{Deserialize, Serialize}; /// A project as served by the hub / consumed by the cli. /// `files` is only filled in for single-project lookups (not lists). #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Project { pub slug: String, pub name: String, /// "app" (default) or "template" — templates are skeletons pulled by agents #[serde(default)] pub kind: String, pub version: String, pub description: String, pub llm_description: String, pub keywords: Vec, pub code_keywords: Vec, pub languages: Vec, pub author: String, pub license: String, pub homepage: String, pub entry_points: Vec, pub install: String, pub run: String, pub model: String, pub prompt_summary: String, pub file_count: i64, pub total_bytes: i64, pub downloads: i64, pub views: i64, pub created_at: String, pub updated_at: String, /// machine specs as free-form json (see nd_core::specs::Specs) #[serde(default)] pub specs: Option, /// maintainer public key (base64 ed25519), set by the first signed upload #[serde(default, skip_serializing_if = "Option::is_none")] pub owner: Option, /// slug of the project this one forked from, if any #[serde(default, skip_serializing_if = "Option::is_none")] pub fork_of: Option, /// short plain-text readme excerpt, filled in for list views / cards #[serde(default, skip_serializing_if = "Option::is_none")] pub readme_excerpt: Option, #[serde(default)] pub files: Vec, } impl Project { /// "2026-10-16T12:00:00+00:00" -> "2026-10-16" pub fn short_updated(&self) -> String { self.updated_at.split('T').next().unwrap_or("").to_string() } /// First 8 chars of the maintainer key, if owned. pub fn owner_short(&self) -> Option { self.owner.as_deref().map(|o| o.chars().take(8).collect()) } /// Canonical url path under /p/: "/" when owned, else "". pub fn url_path(&self) -> String { match self.owner_short() { Some(o) => format!("{o}/{}", self.slug), None => self.slug.clone(), } } /// The ref `nd download ` accepts — same shape as url_path. pub fn download_ref(&self) -> String { self.url_path() } } /// A review/feedback entry on a project — what happened when someone /// (often an llm agent) downloaded and tried it. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Review { pub id: i64, pub slug: String, pub author: String, /// suggested: "works" | "issues" | "broken" | "notes" (free text allowed) pub verdict: String, pub body: String, pub created_at: String, } impl Review { /// "2026-10-16T12:00:00+00:00" -> "2026-10-16" pub fn short_date(&self) -> String { self.created_at.split('T').next().unwrap_or("").to_string() } } /// A maintainer-posted update (changelog entry) on a project. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ProjectUpdate { pub id: i64, pub slug: String, pub author: String, pub title: String, pub body: String, pub created_at: String, } impl ProjectUpdate { /// "2026-10-16T12:00:00+00:00" -> "2026-10-16" pub fn short_date(&self) -> String { self.created_at.split('T').next().unwrap_or("").to_string() } }