vx_cli/commands/
sync.rs

1// Sync command implementation
2
3use crate::ui::UI;
4
5use vx_core::{FigmentConfigManager, PluginRegistry, Result, VxError};
6
7pub async fn handle(
8    registry: &PluginRegistry,
9    check: bool,
10    force: bool,
11    dry_run: bool,
12    verbose: bool,
13    no_parallel: bool,
14    no_auto_install: bool,
15) -> Result<()> {
16    let spinner = UI::new_spinner("Reading project configuration...");
17
18    // Check if .vx.toml exists
19    let config_path = std::env::current_dir()
20        .map_err(|e| VxError::Other {
21            message: format!("Failed to get current directory: {}", e),
22        })?
23        .join(".vx.toml");
24
25    if !config_path.exists() {
26        spinner.finish_and_clear();
27        return handle_no_config().await;
28    }
29
30    // Load project configuration
31    let config_manager = FigmentConfigManager::new()?;
32    let config = config_manager.config();
33
34    spinner.finish_and_clear();
35
36    if config.tools.is_empty() {
37        UI::warn("No tools defined in .vx.toml");
38        return Ok(());
39    }
40
41    UI::header("Project Sync");
42    UI::info(&format!("Found {} tools to sync", config.tools.len()));
43
44    // Analyze tools and their status
45    let mut sync_plan = Vec::new();
46    for (tool_name, tool_config) in &config.tools {
47        let version = tool_config.version.as_deref().unwrap_or("latest");
48        let status = check_tool_status(registry, tool_name, version).await?;
49        sync_plan.push(SyncItem {
50            name: tool_name.clone(),
51            version: version.to_string(),
52            status,
53        });
54    }
55
56    if check {
57        return display_check_results(&sync_plan);
58    }
59
60    if dry_run {
61        return display_dry_run(&sync_plan);
62    }
63
64    // Execute sync
65    execute_sync(
66        registry,
67        &sync_plan,
68        force,
69        verbose,
70        !no_parallel,
71        !no_auto_install,
72    )
73    .await
74}
75
76async fn handle_no_config() -> Result<()> {
77    UI::warn("No .vx.toml configuration file found");
78    UI::info("Detecting project type...");
79
80    let current_dir = std::env::current_dir().map_err(|e| VxError::Other {
81        message: format!("Failed to get current directory: {}", e),
82    })?;
83
84    // Check for common project files
85    if current_dir.join("package.json").exists() {
86        suggest_node_config().await?;
87    } else if current_dir.join("pyproject.toml").exists()
88        || current_dir.join("requirements.txt").exists()
89    {
90        suggest_python_config().await?;
91    } else if current_dir.join("go.mod").exists() {
92        suggest_go_config().await?;
93    } else if current_dir.join("Cargo.toml").exists() {
94        suggest_rust_config().await?;
95    } else {
96        UI::info("No project type detected. Use 'vx init' to create a configuration file.");
97    }
98
99    Ok(())
100}
101
102async fn suggest_node_config() -> Result<()> {
103    UI::info("🔍 Detected Node.js project (package.json found)");
104    UI::info("💡 Suggested configuration:");
105    println!();
106    println!("[tools]");
107    println!("node = \"18.17.0\"  # LTS version");
108    println!("npm = \"latest\"");
109    println!();
110    UI::info("Run 'vx init --template node' to create .vx.toml with these settings");
111    Ok(())
112}
113
114async fn suggest_python_config() -> Result<()> {
115    UI::info("🔍 Detected Python project");
116    UI::info("💡 Suggested configuration:");
117    println!();
118    println!("[tools]");
119    println!("python = \"3.11\"");
120    println!("uv = \"latest\"");
121    println!();
122    UI::info("Run 'vx init --template python' to create .vx.toml with these settings");
123    Ok(())
124}
125
126async fn suggest_go_config() -> Result<()> {
127    UI::info("🔍 Detected Go project (go.mod found)");
128    UI::info("💡 Suggested configuration:");
129    println!();
130    println!("[tools]");
131    println!("go = \"latest\"");
132    println!();
133    UI::info("Run 'vx init --template go' to create .vx.toml with these settings");
134    Ok(())
135}
136
137async fn suggest_rust_config() -> Result<()> {
138    UI::info("🔍 Detected Rust project (Cargo.toml found)");
139    UI::info("💡 Suggested configuration:");
140    println!();
141    println!("[tools]");
142    println!("cargo = \"latest\"");
143    println!();
144    UI::info("Run 'vx init --template rust' to create .vx.toml with these settings");
145    Ok(())
146}
147
148#[derive(Debug)]
149struct SyncItem {
150    name: String,
151    version: String,
152    status: ToolStatus,
153}
154
155#[derive(Debug)]
156#[allow(dead_code)]
157enum ToolStatus {
158    Installed(String), // version
159    NotInstalled,
160    NotSupported,
161}
162
163async fn check_tool_status(
164    registry: &PluginRegistry,
165    tool_name: &str,
166    _version: &str,
167) -> Result<ToolStatus> {
168    if registry.supports_tool(tool_name) {
169        // TODO: Check if specific version is installed
170        // For now, just return NotInstalled
171        Ok(ToolStatus::NotInstalled)
172    } else {
173        Ok(ToolStatus::NotSupported)
174    }
175}
176
177fn display_check_results(sync_plan: &[SyncItem]) -> Result<()> {
178    UI::info("🔍 Checking project requirements...");
179    println!();
180
181    let mut installed_count = 0;
182    let mut missing_count = 0;
183    let mut unsupported_count = 0;
184
185    println!("Required tools:");
186    for item in sync_plan {
187        match &item.status {
188            ToolStatus::Installed(version) => {
189                println!(
190                    "  ✅ {}@{} (installed: {})",
191                    item.name, item.version, version
192                );
193                installed_count += 1;
194            }
195            ToolStatus::NotInstalled => {
196                println!("  ❌ {}@{} (not installed)", item.name, item.version);
197                missing_count += 1;
198            }
199            ToolStatus::NotSupported => {
200                println!("  ⚠️  {}@{} (not supported)", item.name, item.version);
201                unsupported_count += 1;
202            }
203        }
204    }
205
206    println!();
207    println!("Summary:");
208    if installed_count > 0 {
209        println!("  - {} tools already installed", installed_count);
210    }
211    if missing_count > 0 {
212        println!("  - {} tools need installation", missing_count);
213    }
214    if unsupported_count > 0 {
215        println!("  - {} tools not supported", unsupported_count);
216    }
217
218    if missing_count > 0 {
219        println!();
220        UI::info("Run 'vx sync' to install missing tools.");
221    }
222
223    Ok(())
224}
225
226fn display_dry_run(sync_plan: &[SyncItem]) -> Result<()> {
227    UI::info("🔍 Sync plan preview:");
228    println!();
229
230    let mut will_install = Vec::new();
231    let mut will_skip = Vec::new();
232
233    for item in sync_plan {
234        match &item.status {
235            ToolStatus::Installed(_) => will_skip.push(item),
236            ToolStatus::NotInstalled => will_install.push(item),
237            ToolStatus::NotSupported => {
238                println!("  ⚠️  {}@{} (not supported)", item.name, item.version);
239            }
240        }
241    }
242
243    if !will_install.is_empty() {
244        println!("Will install:");
245        for item in &will_install {
246            println!("  📦 {}@{}", item.name, item.version);
247            println!(
248                "    - Install to: ~/.vx/tools/{}/{}/",
249                item.name, item.version
250            );
251        }
252        println!();
253    }
254
255    if !will_skip.is_empty() {
256        println!("Will skip:");
257        for item in &will_skip {
258            println!("  ⏭️  {}@{} (already installed)", item.name, item.version);
259        }
260        println!();
261    }
262
263    UI::info("Run 'vx sync' to execute this plan.");
264    Ok(())
265}
266
267async fn execute_sync(
268    _registry: &PluginRegistry,
269    sync_plan: &[SyncItem],
270    _force: bool,
271    verbose: bool,
272    _parallel: bool,
273    _auto_install: bool,
274) -> Result<()> {
275    UI::info("📦 Installing tools...");
276
277    let mut success_count = 0;
278    let mut error_count = 0;
279
280    for item in sync_plan {
281        match &item.status {
282            ToolStatus::Installed(_) if !_force => {
283                if verbose {
284                    UI::info(&format!(
285                        "⏭️  {}@{} (already installed)",
286                        item.name, item.version
287                    ));
288                }
289            }
290            ToolStatus::NotInstalled | ToolStatus::Installed(_) => {
291                if verbose {
292                    UI::info(&format!("⬇️  Installing {}@{}...", item.name, item.version));
293                }
294
295                // TODO: Implement actual installation
296                // For now, just simulate
297                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
298
299                UI::success(&format!(
300                    "✅ {}@{} installed successfully",
301                    item.name, item.version
302                ));
303                success_count += 1;
304            }
305            ToolStatus::NotSupported => {
306                UI::error(&format!(
307                    "❌ {}@{} (not supported)",
308                    item.name, item.version
309                ));
310                error_count += 1;
311            }
312        }
313    }
314
315    println!();
316    if error_count == 0 {
317        UI::success("🎉 Project sync completed! All tools are ready.");
318
319        if success_count > 0 {
320            println!();
321            println!("Next steps:");
322            for item in sync_plan {
323                if matches!(item.status, ToolStatus::NotInstalled) {
324                    println!("  vx {} --version", item.name);
325                }
326            }
327        }
328    } else {
329        UI::error(&format!("❌ Sync completed with {} errors", error_count));
330    }
331
332    Ok(())
333}