kotoba_package_manager/
lib.rs1use anyhow::Result;
8use kotoba_cid::CidCalculator;
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12pub mod config;
13pub mod dependency;
14pub mod registry;
15pub mod installer;
16pub mod lockfile;
17pub mod cache;
18mod resolver;
19
20pub use dependency::{DependencyInfo, Package, PackageSource, ProjectConfig};
21use lockfile::Lockfile;
22use resolver::Resolver;
23
24#[derive(Debug)]
26pub struct PackageManager {
27 config: config::Config,
28 registry: registry::Registry,
29 cache: cache::Cache,
30 installer: installer::Installer,
31}
32
33impl PackageManager {
34 pub async fn new() -> Result<Self> {
36 let config = config::Config::load()?;
37 let registry = registry::Registry::new(&config)?;
38 let cache = cache::Cache::new(&config)?;
39 let installer = installer::Installer::new(cache.clone());
40
41 Ok(Self {
42 config,
43 registry,
44 cache,
45 installer,
46 })
47 }
48
49 pub async fn install(&self) -> Result<()> {
51 let calculator = CidCalculator::default();
52 let lockfile_path = PathBuf::from("kotoba.lock");
53
54 let config_path = PathBuf::from("kotoba.toml");
56 if !config_path.exists() {
57 println!("kotoba.toml not found. Nothing to install.");
58 return Ok(());
59 }
60 let toml_content = tokio::fs::read_to_string(&config_path).await?;
61 let project_config: ProjectConfig = toml::from_str(&toml_content)?;
62
63 let mut packages_to_install = project_config.dependencies.clone();
65 let mut locked_packages: HashMap<String, crate::lockfile::LockedPackage> = HashMap::new();
66
67 if let Some(lockfile) = Lockfile::read_from_disk(&lockfile_path).await? {
68 println!("Verifying lockfile...");
69 locked_packages = lockfile.packages;
70
71 for (name, _dep_info) in &project_config.dependencies {
72 if let Some(locked) = locked_packages.get(name) {
73 let package_dir = PathBuf::from("node_modules").join(name);
74 if self.cache.get_by_cid(&locked.cid).await?.is_some() && package_dir.exists() {
75 packages_to_install.remove(name);
77 }
78 }
79 }
80 }
81
82 if packages_to_install.is_empty() {
83 println!("All dependencies are up to date.");
84 return Ok(());
86 }
87
88 println!("Resolving and installing {} packages...", packages_to_install.len());
90 let mut resolved_packages = self.resolver().resolve(&packages_to_install).await?;
91
92 for package in &mut resolved_packages {
93 if let Some(url) = &package.tarball_url {
94 let tarball_bytes = reqwest::get(url).await?.bytes().await?.to_vec();
95 let cid = calculator.compute_cid(&tarball_bytes)?;
96 package.cid = Some(cid.to_string());
97 self.cache.store_by_cid(&cid.to_string(), &tarball_bytes).await?;
98 }
99 }
100
101 self.installer.install(resolved_packages.clone()).await?;
103
104 let mut final_packages_map: HashMap<String, Package> = HashMap::new();
106
107 for (name, _dep_info) in &project_config.dependencies {
109 if let Some(locked) = locked_packages.get(name) {
110 if packages_to_install.get(name).is_none() { final_packages_map.insert(name.clone(), Package {
112 name: name.clone(),
113 version: locked.version.clone(),
114 source: locked.source.clone(),
115 cid: Some(locked.cid.clone()),
116 tarball_url: None,
117 description: None,
118 authors: vec![],
119 dependencies: HashMap::new(), dev_dependencies: HashMap::new(),
121 repository: None,
122 license: None,
123 keywords: vec![],
124 });
125 }
126 }
127 }
128
129 for pkg in resolved_packages {
131 final_packages_map.insert(pkg.name.clone(), pkg);
132 }
133
134 let lockfile = Lockfile::from_packages(&final_packages_map.values().cloned().collect::<Vec<_>>());
135 lockfile.write_to_disk(&lockfile_path).await?;
136
137 println!("Installation completed!");
138 Ok(())
139 }
140
141 pub async fn uninstall(&self, packages: Vec<String>) -> Result<()> {
143 println!("Uninstalling packages: {:?}", packages);
144 Ok(())
146 }
147
148 pub async fn search(&self, query: &str) -> Result<Vec<Package>> {
150 println!("Searching for packages: {}", query);
151 self.registry.search(query).await.map_err(Into::into)
152 }
153
154 pub async fn init(&self, name: Option<String>) -> Result<()> {
156 let project_name = name.unwrap_or_else(|| "my-kotoba-project".to_string());
157
158 let config = ProjectConfig {
159 name: project_name.clone(),
160 version: "0.1.0".to_string(),
161 description: Some("A Kotoba project".to_string()),
162 dependencies: HashMap::new(),
163 dev_dependencies: HashMap::new(),
164 scripts: [
165 ("test".to_string(), "kotoba test".to_string()),
166 ("fmt".to_string(), "kotoba fmt".to_string()),
167 ("lint".to_string(), "kotoba lint".to_string()),
168 ].into_iter().collect(),
169 };
170
171 let config_path = PathBuf::from("kotoba.toml");
173 let toml_content = toml::to_string(&config)?;
174 tokio::fs::write(&config_path, toml_content).await?;
175
176 tokio::fs::create_dir_all("src").await?;
178
179 let main_content = format!(r#"// {} - Kotoba Project
181// This is your main entry point
182
183fn main() {{
184 println("Hello, {}!");
185}}
186
187// Export your public API
188pub fn greet(name: String) -> String {{
189 format("Hello, {{}}!", name)
190}}
191"#, project_name, project_name);
192
193 tokio::fs::write("src/main.kotoba", main_content).await?;
194
195 println!("✅ Initialized Kotoba project: {}", project_name);
196 println!("📁 Created kotoba.toml and src/main.kotoba");
197 println!("🚀 Run 'kotoba run src/main.kotoba' to get started!");
198
199 Ok(())
200 }
201
202 pub async fn clear_cache(&self) -> Result<()> {
204 self.cache.clear().await?;
205 println!("✅ Cache cleared!");
206 Ok(())
207 }
208
209 fn resolver(&self) -> resolver::Resolver {
211 resolver::Resolver::new()
212 }
213
214 fn installer(&self) -> &installer::Installer {
216 &self.installer
217 }
218}
219
220pub async fn init_project(name: Option<String>) -> Result<()> {
222 let pm = PackageManager::new().await?;
223 pm.init(name).await
224}
225
226pub async fn install_packages() -> Result<()> {
227 let pm = PackageManager::new().await?;
228 pm.install().await
229}
230
231pub async fn search_packages(query: &str) -> Result<Vec<Package>> {
232 let pm = PackageManager::new().await?;
233 pm.search(query).await
234}