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<String>,
pub code_keywords: Vec<String>,
pub languages: Vec<String>,
pub author: String,
pub license: String,
pub homepage: String,
pub entry_points: Vec<String>,
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<serde_json::Value>,
/// maintainer public key (base64 ed25519), set by the first signed upload
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
/// slug of the project this one forked from, if any
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_of: Option<String>,
/// short plain-text readme excerpt, filled in for list views / cards
#[serde(default, skip_serializing_if = "Option::is_none")]
pub readme_excerpt: Option<String>,
#[serde(default)]
pub files: Vec<String>,
}
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<String> {
self.owner.as_deref().map(|o| o.chars().take(8).collect())
}
/// Canonical url path under /p/: "<owner8>/<slug>" when owned, else "<slug>".
pub fn url_path(&self) -> String {
match self.owner_short() {
Some(o) => format!("{o}/{}", self.slug),
None => self.slug.clone(),
}
}
/// The ref `nd download <ref>` 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()
}
}