1use colored::*;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7use toml;
8
9use super::speed_test::{SpeedTestResult, SpeedTester};
10use super::Logger;
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct Registry {
14 pub registry: String,
15 pub home: Option<String>,
16}
17
18#[derive(Debug, Clone)]
19pub struct Store {
20 pub registries: HashMap<String, Registry>,
21}
22
23impl Store {
24 pub async fn load() -> Self {
25 let config_path = get_config_path();
26 Logger::info(&format!("Config file path: {}", config_path.display()));
27
28 if !config_path.exists() {
29 Logger::info("Config file not found, creating default configuration...");
30 Self::create_default_config(&config_path).await;
31 }
32
33 match fs::read_to_string(&config_path).await {
34 Ok(contents) => {
35 match toml::from_str(&contents) {
36 Ok(registries) => Self { registries },
37 Err(e) => {
38 Logger::error(&format!("Failed to parse config file: {}", e));
39 Self::create_default_store()
40 }
41 }
42 }
43 Err(e) => {
44 Logger::error(&format!("Failed to read config file: {}", e));
45 Self::create_default_store()
46 }
47 }
48 }
49
50 async fn create_default_config(config_path: &Path) {
51 let default_registries = HashMap::from([
52 (
53 "npm".to_string(),
54 Registry {
55 registry: "https://registry.npmjs.org/".to_string(),
56 home: Some("https://www.npmjs.org".to_string()),
57 },
58 ),
59 (
60 "yarn".to_string(),
61 Registry {
62 registry: "https://registry.yarnpkg.com/".to_string(),
63 home: Some("https://yarnpkg.com".to_string()),
64 },
65 ),
66 (
67 "taobao".to_string(),
68 Registry {
69 registry: "https://registry.npmmirror.com/".to_string(),
70 home: Some("https://npmmirror.com/".to_string()),
71 },
72 ),
73 (
74 "tencent".to_string(),
75 Registry {
76 registry: "https://mirrors.cloud.tencent.com/npm/".to_string(),
77 home: Some("https://mirrors.cloud.tencent.com/npm/".to_string()),
78 },
79 ),
80 (
81 "npmMirror".to_string(),
82 Registry {
83 registry: "https://skimdb.npmjs.com/registry/".to_string(),
84 home: Some("https://skimdb.npmjs.com".to_string()),
85 },
86 ),
87 (
88 "github".to_string(),
89 Registry {
90 registry: "https://npm.pkg.github.com/".to_string(),
91 home: Some("https://github.com".to_string()),
92 },
93 ),
94 ]);
95
96 if let Some(parent) = config_path.parent() {
97 if !parent.exists() {
98 if let Err(e) = fs::create_dir_all(parent).await {
99 Logger::error(&format!("Failed to create config directory: {}", e));
100 return;
101 }
102 }
103 }
104
105 let toml = toml::to_string(&default_registries).unwrap();
106 if let Err(e) = fs::write(config_path, toml).await {
107 Logger::error(&format!("Failed to write default config: {}", e));
108 }
109 }
110
111 fn create_default_store() -> Self {
112 Self {
113 registries: HashMap::from([
114 (
115 "npm".to_string(),
116 Registry {
117 registry: "https://registry.npmjs.org/".to_string(),
118 home: Some("https://www.npmjs.org".to_string()),
119 },
120 ),
121 (
122 "yarn".to_string(),
123 Registry {
124 registry: "https://registry.yarnpkg.com/".to_string(),
125 home: Some("https://yarnpkg.com".to_string()),
126 },
127 ),
128 (
129 "taobao".to_string(),
130 Registry {
131 registry: "https://registry.npmmirror.com/".to_string(),
132 home: Some("https://npmmirror.com/".to_string()),
133 },
134 ),
135 (
136 "tencent".to_string(),
137 Registry {
138 registry: "https://mirrors.cloud.tencent.com/npm/".to_string(),
139 home: Some("https://mirrors.cloud.tencent.com/npm/".to_string()),
140 },
141 ),
142 (
143 "npmMirror".to_string(),
144 Registry {
145 registry: "https://skimdb.npmjs.com/registry/".to_string(),
146 home: Some("https://skimdb.npmjs.com".to_string()),
147 },
148 ),
149 (
150 "github".to_string(),
151 Registry {
152 registry: "https://npm.pkg.github.com/".to_string(),
153 home: Some("https://github.com".to_string()),
154 },
155 ),
156 ])
157 }
158 }
159
160 pub async fn save(&self) {
161 let config_path = get_config_path();
162 let content = toml::to_string_pretty(&self.registries).unwrap();
163 if let Err(e) = fs::write(&config_path, content).await {
164 Logger::error(&format!("Failed to save config: {}", e));
165 }
166 }
167
168 pub async fn get_current_registry(&self, is_local: bool) -> Option<String> {
169 let npmrc_path = if is_local {
170 ".npmrc".to_string()
171 } else {
172 dirs::home_dir()
173 .map(|path| path.join(".npmrc").to_string_lossy().to_string())
174 .expect("Failed to get home directory")
175 };
176
177 if Path::new(&npmrc_path).exists() {
178 if let Ok(content) = fs::read_to_string(&npmrc_path).await {
179 let re = Regex::new(r"(?m)^\s*registry\s*=\s*(.+?)\s*$").unwrap();
180 if let Some(captures) = re.captures(&content) {
181 let registry_url = captures.get(1).unwrap().as_str().to_string();
182 for (name, registry) in &self.registries {
184 if registry.registry == registry_url {
185 return Some(name.clone());
186 }
187 }
188 }
189 }
190 }
191 None
192 }
193
194 pub async fn list_registries(&self) {
195 Logger::list("Available registries:");
196
197 let current_global = self.get_current_registry(false).await;
199 let current_local = self.get_current_registry(true).await;
200
201 for (name, registry) in self.registries.iter() {
202 let mut tags = Vec::new();
203
204 if let Some(current_global_name) = ¤t_global {
205 if name == current_global_name {
206 tags.push("[GLOBAL]".white().on_blue());
207 }
208 }
209
210 if let Some(current_local_name) = ¤t_local {
211 if name == current_local_name {
212 tags.push("[LOCAL]".white().on_green());
213 }
214 }
215
216 let tags_str = if !tags.is_empty() {
217 format!(
218 " {}",
219 tags.iter()
220 .map(|t| t.to_string())
221 .collect::<Vec<_>>()
222 .join(" ")
223 )
224 } else {
225 String::new()
226 };
227
228 println!(
229 "{} -> {}{}",
230 name.green().bold(),
231 registry.registry.yellow(),
232 tags_str
233 );
234 }
235 }
236
237 pub fn set_current_use(&mut self, name: &str, is_local: bool) {
238 if self.registries.contains_key(name) {
239 Logger::info(&format!(
240 "Switched to registry: {} ({})",
241 name.green().bold(),
242 if is_local {
243 "local".yellow()
244 } else {
245 "global".yellow()
246 }
247 ));
248 } else {
249 Logger::error(&format!("Registry not found: {}", name));
250 }
251 }
252
253 pub async fn test_registry_speed(&self) -> Vec<SpeedTestResult> {
254 let tester = SpeedTester::new();
255 let registries: Vec<(String, String)> = self
256 .registries
257 .iter()
258 .map(|(name, reg)| (name.clone(), reg.registry.clone()))
259 .collect();
260
261 tester.test_all(®istries).await
262 }
263}
264
265fn get_config_path() -> PathBuf {
266 let home = dirs::home_dir().expect("Could not find home directory");
267 home.join(".config")
268 .join("rust-nrm")
269 .join("registries.toml")
270}