zernel/commands/
marketplace.rs1use anyhow::Result;
8use clap::Subcommand;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12#[derive(Subcommand)]
13pub enum MarketplaceCommands {
14 Publish {
16 path: String,
18 #[arg(long)]
20 name: String,
21 #[arg(long, default_value = "")]
23 description: String,
24 #[arg(long, default_value = "apache2")]
26 license: String,
27 },
28 Browse {
30 #[arg(default_value = "")]
32 query: String,
33 },
34 Download {
36 name: String,
38 #[arg(long, default_value = ".")]
40 to: String,
41 },
42 Deploy {
44 name: String,
46 #[arg(long, default_value = "8080")]
48 port: u16,
49 },
50 My,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55struct MarketplaceEntry {
56 name: String,
57 description: String,
58 license: String,
59 path: String,
60 size_bytes: u64,
61 published_at: String,
62 downloads: u64,
63}
64
65fn marketplace_dir() -> PathBuf {
66 let dir = crate::experiments::tracker::zernel_dir().join("marketplace");
67 std::fs::create_dir_all(&dir).ok();
68 dir
69}
70
71fn marketplace_registry() -> PathBuf {
72 marketplace_dir().join("registry.json")
73}
74
75fn load_marketplace() -> Vec<MarketplaceEntry> {
76 marketplace_registry()
77 .exists()
78 .then(|| {
79 std::fs::read_to_string(marketplace_registry())
80 .ok()
81 .and_then(|s| serde_json::from_str(&s).ok())
82 })
83 .flatten()
84 .unwrap_or_default()
85}
86
87fn save_marketplace(entries: &[MarketplaceEntry]) -> Result<()> {
88 std::fs::write(
89 marketplace_registry(),
90 serde_json::to_string_pretty(entries)?,
91 )?;
92 Ok(())
93}
94
95pub async fn run(cmd: MarketplaceCommands) -> Result<()> {
96 match cmd {
97 MarketplaceCommands::Publish {
98 path,
99 name,
100 description,
101 license,
102 } => {
103 let source = std::path::Path::new(&path);
104 if !source.exists() {
105 anyhow::bail!("path not found: {path}");
106 }
107
108 println!("Zernel Marketplace — Publish");
109 println!("{}", "=".repeat(50));
110 println!(" Name: {name}");
111 println!(
112 " Description: {}",
113 if description.is_empty() {
114 "(none)"
115 } else {
116 &description
117 }
118 );
119 println!(" License: {license}");
120 println!(" Source: {path}");
121
122 let dest = marketplace_dir().join(&name);
124 std::fs::create_dir_all(&dest)?;
125
126 let size = if source.is_file() {
127 let fname = source.file_name().unwrap_or_default();
128 std::fs::copy(source, dest.join(fname))?;
129 source.metadata()?.len()
130 } else {
131 let _ = std::process::Command::new("cp")
133 .args(["-r", &path, &dest.to_string_lossy()])
134 .status();
135 0
136 };
137
138 let mut registry = load_marketplace();
139 registry.retain(|e| e.name != name);
140 registry.push(MarketplaceEntry {
141 name: name.clone(),
142 description,
143 license,
144 path: dest.to_string_lossy().to_string(),
145 size_bytes: size,
146 published_at: chrono::Utc::now().to_rfc3339(),
147 downloads: 0,
148 });
149 save_marketplace(®istry)?;
150
151 println!();
152 println!(" Published: {name}");
153 println!(" Browse: zernel marketplace browse");
154 }
155
156 MarketplaceCommands::Browse { query } => {
157 let registry = load_marketplace();
158
159 println!("Zernel Marketplace");
160 println!("{}", "=".repeat(60));
161
162 if registry.is_empty() {
163 println!(" No models published yet.");
164 println!(" Publish one: zernel marketplace publish ./model --name my-model");
165 return Ok(());
166 }
167
168 let filtered: Vec<&MarketplaceEntry> = if query.is_empty() {
169 registry.iter().collect()
170 } else {
171 let q = query.to_lowercase();
172 registry
173 .iter()
174 .filter(|e| {
175 e.name.to_lowercase().contains(&q)
176 || e.description.to_lowercase().contains(&q)
177 })
178 .collect()
179 };
180
181 for entry in &filtered {
182 let size = if entry.size_bytes > 0 {
183 format!("{:.1} MB", entry.size_bytes as f64 / (1024.0 * 1024.0))
184 } else {
185 "N/A".into()
186 };
187 println!(" {} ({})", entry.name, entry.license);
188 if !entry.description.is_empty() {
189 println!(" {}", entry.description);
190 }
191 println!(
192 " Size: {} | Published: {} | Downloads: {}",
193 size,
194 &entry.published_at[..10],
195 entry.downloads
196 );
197 println!();
198 }
199
200 if filtered.is_empty() {
201 println!(" No models matching '{query}'");
202 }
203 }
204
205 MarketplaceCommands::Download { name, to } => {
206 let registry = load_marketplace();
207 let entry = registry
208 .iter()
209 .find(|e| e.name == name)
210 .ok_or_else(|| anyhow::anyhow!("model not found: {name}"))?;
211
212 println!("Downloading: {name} → {to}");
213 let _ = std::process::Command::new("cp")
214 .args(["-r", &entry.path, &to])
215 .status();
216 println!("Done.");
217 }
218
219 MarketplaceCommands::Deploy { name, port } => {
220 println!("Deploying {name} on port {port}...");
221 println!(" Use: zernel serve start {name} --port {port}");
222 }
223
224 MarketplaceCommands::My => {
225 let registry = load_marketplace();
226 if registry.is_empty() {
227 println!("No published models.");
228 return Ok(());
229 }
230
231 println!("My Published Models");
232 println!("{}", "=".repeat(50));
233 for entry in ®istry {
234 println!(
235 " {} — {} ({})",
236 entry.name,
237 entry.license,
238 &entry.published_at[..10]
239 );
240 }
241 }
242 }
243 Ok(())
244}