zernel/commands/
marketplace.rs

1// Copyright (C) 2026 Dyber, Inc. — Proprietary
2
3//! zernel marketplace — Share and monetize ML models
4//!
5//! A decentralized marketplace for ML models and datasets.
6
7use anyhow::Result;
8use clap::Subcommand;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12#[derive(Subcommand)]
13pub enum MarketplaceCommands {
14    /// Publish a model to the marketplace
15    Publish {
16        /// Path to model
17        path: String,
18        /// Model name
19        #[arg(long)]
20        name: String,
21        /// Description
22        #[arg(long, default_value = "")]
23        description: String,
24        /// License (mit, apache2, proprietary)
25        #[arg(long, default_value = "apache2")]
26        license: String,
27    },
28    /// Browse available models
29    Browse {
30        /// Search query
31        #[arg(default_value = "")]
32        query: String,
33    },
34    /// Download a model from the marketplace
35    Download {
36        /// Model name (author/name)
37        name: String,
38        /// Destination path
39        #[arg(long, default_value = ".")]
40        to: String,
41    },
42    /// Deploy a marketplace model
43    Deploy {
44        /// Model name
45        name: String,
46        /// Port
47        #[arg(long, default_value = "8080")]
48        port: u16,
49    },
50    /// Show my published models
51    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            // Copy to marketplace directory
123            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                // Copy directory
132                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(&registry)?;
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 &registry {
234                println!(
235                    "  {} — {} ({})",
236                    entry.name,
237                    entry.license,
238                    &entry.published_at[..10]
239                );
240            }
241        }
242    }
243    Ok(())
244}