Skip to main content

zoi_cli/cmd/uninstall/
mod.rs

1//! Logic for the `uninstall` command.
2
3use std::collections::HashSet;
4use std::fs;
5use std::path::Path;
6
7/// Command-line arguments for the `uninstall` command.
8pub mod args;
9
10use anyhow::{Result, anyhow};
11use colored::Colorize;
12use mlua::LuaSerdeExt;
13use serde_json::json;
14use walkdir::WalkDir;
15
16use crate::cmd::{utils, ux};
17use crate::pkg::{self, lock, transaction, types};
18
19/// Runs the `uninstall` command to remove one or more packages.
20///
21/// # Errors
22///
23/// Returns an error if:
24/// - The --save flag is used with a non-project scope.
25/// - Package resolution fails for any of the provided names.
26/// - Uninstallation fails for any resolved package.
27/// - A transaction cannot be started, recorded, or committed.
28pub fn run(
29    package_names: &[String],
30    scope: Option<crate::cli::InstallScope>,
31    local: bool,
32    global: bool,
33    save: bool,
34    yes: bool,
35    recursive: bool,
36    plugin_manager: Option<&crate::pkg::plugin::PluginManager>,
37    explain: bool,
38    plan_json: bool,
39    dry_run: bool
40) -> Result<()> {
41    let mut scope_override = scope.map(|s| match s {
42        crate::cli::InstallScope::User => types::Scope::User,
43        crate::cli::InstallScope::System => types::Scope::System,
44        crate::cli::InstallScope::Project => types::Scope::Project
45    });
46
47    if local {
48        scope_override = Some(types::Scope::Project);
49    } else if global || scope_override.is_none() {
50        scope_override = Some(types::Scope::User);
51    }
52
53    if save && scope_override != Some(types::Scope::Project) {
54        return Err(anyhow!(
55            "The --save flag can only be used with project-scoped uninstalls."
56        ));
57    }
58
59    let installed_packages = pkg::local::get_installed_packages()?;
60
61    let mut manifests_to_uninstall: Vec<types::InstallManifest> = Vec::new();
62    let mut failed_resolution = false;
63
64    let expanded_names =
65        utils::expand_split_packages(package_names, "Uninstalling")?;
66
67    for name in &expanded_names {
68        if let Err(e) = resolve_and_add_manifest(
69            name,
70            &installed_packages,
71            &mut manifests_to_uninstall,
72            scope_override,
73            yes
74        ) {
75            eprintln!("{e}");
76            failed_resolution = true;
77        }
78    }
79
80    if failed_resolution {
81        return Err(anyhow!(
82            "Failed to resolve some packages for uninstallation."
83        ));
84    }
85
86    if recursive {
87        collect_recursive_uninstalls(
88            &mut manifests_to_uninstall,
89            &installed_packages
90        )?;
91    }
92
93    if manifests_to_uninstall.is_empty() {
94        println!("No packages to uninstall.");
95        ux::print_transaction_summary(&ux::TransactionSummary {
96            command: "uninstall".to_string(),
97            success: 0,
98            failed: 0,
99            skipped: 0
100        });
101        return Ok(());
102    }
103
104    manifests_to_uninstall.sort_by(|a, b| {
105        a.name
106            .cmp(&b.name)
107            .then_with(|| scope_rank(a.scope).cmp(&scope_rank(b.scope)))
108            .then_with(|| a.registry_handle.cmp(&b.registry_handle))
109            .then_with(|| a.repo.cmp(&b.repo))
110            .then_with(|| a.sub_package.cmp(&b.sub_package))
111    });
112    manifests_to_uninstall.dedup_by(|a, b| {
113        a.name == b.name
114            && a.sub_package == b.sub_package
115            && a.repo == b.repo
116            && a.registry_handle == b.registry_handle
117            && a.scope == b.scope
118    });
119
120    let mut total_size_freed_bytes: u64 = 0;
121    for manifest in &manifests_to_uninstall {
122        if let Some(size) = manifest.installed_size
123            && size > 0
124        {
125            total_size_freed_bytes += size;
126            continue;
127        }
128
129        let Ok(version_dir) = pkg::local::get_package_version_dir(
130            manifest.scope,
131            &manifest.registry_handle,
132            &manifest.repo,
133            &manifest.name,
134            &manifest.version
135        ) else {
136            continue;
137        };
138
139        let mut package_size: u64 = 0;
140        for file_path_str in &manifest.installed_files {
141            let Ok(expanded) = pkg::utils::expand_placeholders(
142                file_path_str,
143                &version_dir,
144                manifest.scope
145            ) else {
146                continue;
147            };
148            let path = Path::new(&expanded);
149            if !path.exists() {
150                continue;
151            }
152            if path.is_dir() {
153                package_size += WalkDir::new(path)
154                    .into_iter()
155                    .filter_map(std::result::Result::ok)
156                    .filter_map(|e| e.metadata().ok())
157                    .filter(std::fs::Metadata::is_file)
158                    .map(|m| m.len())
159                    .sum::<u64>();
160            } else if let Ok(metadata) = fs::metadata(path) {
161                package_size += metadata.len();
162            }
163        }
164        total_size_freed_bytes += package_size;
165    }
166
167    println!("Packages to remove:");
168    for manifest in &manifests_to_uninstall {
169        let source_str = if let Some(sub) = &manifest.sub_package {
170            format!(
171                "#{}@{}/{}:{}",
172                manifest.registry_handle, manifest.repo, manifest.name, sub
173            )
174        } else {
175            format!(
176                "#{}@{}/{}",
177                manifest.registry_handle, manifest.repo, manifest.name
178            )
179        };
180        println!("  - {source_str}");
181    }
182
183    println!(
184        "\nTotal size to be freed: {}",
185        crate::pkg::utils::format_bytes(total_size_freed_bytes)
186    );
187
188    let removal_ids: std::collections::HashSet<String> = manifests_to_uninstall
189        .iter()
190        .map(removal_identity)
191        .collect();
192    let mut dangerous = Vec::new();
193    let mut impact_json = Vec::new();
194    for manifest in &manifests_to_uninstall {
195        let package_dir = pkg::local::get_package_dir(
196            manifest.scope,
197            &manifest.registry_handle,
198            &manifest.repo,
199            &manifest.name
200        )?;
201        let external_dependents = collect_external_dependents(
202            &removal_ids,
203            pkg::local::get_dependents(&package_dir)?
204        );
205
206        let source = if let Some(sub) = &manifest.sub_package {
207            format!(
208                "#{}@{}/{}:{}",
209                manifest.registry_handle, manifest.repo, manifest.name, sub
210            )
211        } else {
212            format!(
213                "#{}@{}/{}",
214                manifest.registry_handle, manifest.repo, manifest.name
215            )
216        };
217
218        if !external_dependents.is_empty() {
219            dangerous.push((source.clone(), external_dependents.clone()));
220        }
221        impact_json.push(json!({
222            "source": source,
223            "name": manifest.name,
224            "version": manifest.version,
225            "sub_package": manifest.sub_package,
226            "scope": format!("{:?}", manifest.scope),
227            "registry": manifest.registry_handle,
228            "repo": manifest.repo,
229            "external_dependents": external_dependents,
230            "installed_files": manifest.installed_files.len(),
231        }));
232    }
233
234    let preflight = ux::PreflightSummary::new("Uninstall preflight")
235        .row(
236            "Scope override",
237            scope_override
238                .map_or_else(|| "None".to_string(), |s| format!("{s:?}"))
239        )
240        .row("Recursive", recursive.to_string())
241        .row("Packages", manifests_to_uninstall.len().to_string())
242        .row("Dangerous removals", dangerous.len().to_string())
243        .row(
244            "Estimated freed size",
245            crate::pkg::utils::format_bytes(total_size_freed_bytes)
246        );
247    ux::print_preflight(&preflight);
248
249    if explain {
250        let mut report = ux::ExplainReport::new("Uninstall explanation");
251        for manifest in &manifests_to_uninstall {
252            let source = if let Some(sub) = &manifest.sub_package {
253                format!(
254                    "#{}@{}/{}:{}",
255                    manifest.registry_handle, manifest.repo, manifest.name, sub
256                )
257            } else {
258                format!(
259                    "#{}@{}/{}",
260                    manifest.registry_handle, manifest.repo, manifest.name
261                )
262            };
263            report = report.item(
264                format!("{} [{}]", source, manifest.version),
265                format!("reason={:?}", manifest.reason),
266                Vec::new()
267            );
268        }
269        if !dangerous.is_empty() {
270            for (source, deps) in &dangerous {
271                report = report.item(
272                    source.clone(),
273                    format!("blocks {} dependent(s)", deps.len()),
274                    deps.iter()
275                        .map(|dep| format!("dependent: {dep}"))
276                        .collect::<Vec<_>>()
277                );
278            }
279        }
280        ux::print_explain(&report);
281    }
282
283    if plan_json {
284        let plan = json!({
285            "recursive": recursive,
286            "scope_override": format!("{:?}", scope_override),
287            "totals": {
288                "packages": manifests_to_uninstall.len(),
289                "dangerous_removals": dangerous.len(),
290                "freed_bytes": total_size_freed_bytes,
291            },
292            "packages": impact_json,
293        });
294        ux::emit_plan_json_v1("uninstall", plan)?;
295    }
296
297    if !dangerous.is_empty() {
298        println!(
299            "\n{} Removing these packages will break dependents:",
300            "Warning".yellow().bold()
301        );
302        for (source, deps) in &dangerous {
303            println!("  - {}", source.cyan());
304            for dep in deps {
305                println!("    * {dep}");
306            }
307        }
308        if !crate::utils::ask_for_confirmation(
309            "Dangerous removal detected. Continue anyway?",
310            yes
311        ) {
312            ux::print_transaction_summary(&ux::TransactionSummary {
313                command: "uninstall".to_string(),
314                success: 0,
315                failed: 0,
316                skipped: manifests_to_uninstall.len()
317            });
318            return Ok(());
319        }
320    }
321
322    if dry_run {
323        println!("\n{}", "Dry-run mode: no changes will be made.".yellow());
324        ux::print_transaction_summary(&ux::TransactionSummary {
325            command: "uninstall".to_string(),
326            success: 0,
327            failed: 0,
328            skipped: manifests_to_uninstall.len()
329        });
330        return Ok(());
331    }
332
333    if !crate::utils::ask_for_confirmation(":: Proceed with removal?", yes) {
334        let _ = lock::release_lock();
335        ux::print_transaction_summary(&ux::TransactionSummary {
336            command: "uninstall".to_string(),
337            success: 0,
338            failed: 0,
339            skipped: manifests_to_uninstall.len()
340        });
341        return Ok(());
342    }
343
344    let mut transaction = transaction::begin()?;
345
346    let mut failed_packages = Vec::new();
347    let mut successfully_uninstalled = Vec::new();
348
349    if let Some(pm) = plugin_manager {
350        pm.set_context(scope_override.unwrap_or_default())?;
351    }
352
353    for manifest in &manifests_to_uninstall {
354        let mut pkg_val = None;
355        if let Some(pm) = plugin_manager {
356            let v = pm
357                .lua
358                .to_value(manifest)
359                .map_err(|e: mlua::Error| anyhow!(e.to_string()))?;
360            pm.trigger_hook("on_pre_uninstall", Some(&v.clone()))?;
361            pkg_val = Some(v);
362        }
363
364        let source_str = if let Some(sub) = &manifest.sub_package {
365            format!(
366                "#{}@{}/{}:{}",
367                manifest.registry_handle, manifest.repo, manifest.name, sub
368            )
369        } else {
370            format!(
371                "#{}@{}/{}",
372                manifest.registry_handle, manifest.repo, manifest.name
373            )
374        };
375
376        println!(
377            "{} Uninstalling package '{}'...",
378            "::".bold().blue(),
379            source_str.blue().bold()
380        );
381
382        match pkg::uninstall::run(
383            &source_str,
384            scope_override,
385            yes,
386            false,
387            false
388        ) {
389            Ok(uninstalled_manifest) => {
390                if let Err(e) = transaction::record_operation(
391                    &mut transaction,
392                    types::TransactionOperation::Uninstall {
393                        manifest: Box::new(uninstalled_manifest)
394                    }
395                ) {
396                    eprintln!(
397                        "Failed to record transaction operation for \
398                         {source_str}: {e}"
399                    );
400                    failed_packages.push(source_str.clone());
401                } else {
402                    successfully_uninstalled.push(source_str.clone());
403                    if let (Some(pm), Some(v)) = (plugin_manager, pkg_val) {
404                        pm.trigger_hook_nonfatal("on_post_uninstall", Some(&v));
405                    }
406                    println!(
407                        "\n{} Uninstallation complete.",
408                        "Success:".green()
409                    );
410                }
411            }
412            Err(e) => {
413                eprintln!("\nError: {e}");
414                failed_packages.push(source_str.clone());
415            }
416        }
417    }
418
419    if !failed_packages.is_empty() {
420        eprintln!("\nError: Uninstallation failed for some packages.");
421        eprintln!("\n{} Rolling back changes...", "::".bold().yellow());
422        if let Err(e) = transaction::rollback(&transaction.id) {
423            eprintln!("\nCRITICAL: Rollback failed: {e}");
424            eprintln!(
425                "The system may be in an inconsistent state. The transaction \
426                 log is at ~/.zoi/transactions/{}.json",
427                transaction.id
428            );
429        } else {
430            println!("\n{} Rollback successful.", "Success:".green().bold());
431        }
432        ux::print_transaction_summary(&ux::TransactionSummary {
433            command: "uninstall".to_string(),
434            success: successfully_uninstalled.len(),
435            failed: failed_packages.len(),
436            skipped: 0
437        });
438        return Err(anyhow!(
439            "Uninstallation failed for: {}",
440            failed_packages.join(", ")
441        ));
442    }
443
444    if let Ok(modified_files) = transaction::get_modified_files(&transaction.id)
445    {
446        let modified_packages =
447            transaction::get_modified_packages(&transaction.id)
448                .unwrap_or_default();
449        let _ = crate::pkg::hooks::global::run_global_hooks(
450            crate::pkg::hooks::global::HookWhen::PostTransaction,
451            &modified_files,
452            &modified_packages,
453            "remove",
454            scope_override.unwrap_or_default()
455        );
456    }
457
458    if let Err(e) = transaction::commit(&transaction.id) {
459        eprintln!("Warning: Failed to commit transaction: {e}");
460    }
461
462    if save {
463        if std::path::Path::new("zoi.lua").exists() {
464            println!(
465                "\n{} Project uses zoi.lua. Automatic saving is not supported \
466                 for Lua configurations.",
467                "Note:".yellow().bold()
468            );
469            println!(
470                "   Please remove the following from your packages() block in \
471                 zoi.lua:"
472            );
473            for pkg in &successfully_uninstalled {
474                println!("   - \"{pkg}\"");
475            }
476        } else if let Err(e) = zoi_project::config::remove_packages_from_config(
477            &successfully_uninstalled
478        ) {
479            eprintln!(
480                "{}: Failed to remove packages from zoi.yaml: {}",
481                "Warning".yellow().bold(),
482                e
483            );
484        }
485    }
486    ux::print_transaction_summary(&ux::TransactionSummary {
487        command: "uninstall".to_string(),
488        success: successfully_uninstalled.len(),
489        failed: 0,
490        skipped: 0
491    });
492    Ok(())
493}
494
495/// Generates a unique string identity for a package to be removed.
496fn removal_identity(manifest: &types::InstallManifest) -> String {
497    if let Some(sub) = &manifest.sub_package {
498        format!("{}@{}:{}", manifest.name, manifest.version, sub)
499    } else {
500        format!("{}@{}", manifest.name, manifest.version)
501    }
502}
503
504/// Returns a rank for the scope to prioritize removal order.
505fn scope_rank(scope: types::Scope) -> u8 {
506    match scope {
507        types::Scope::Project => 0,
508        types::Scope::User => 1,
509        types::Scope::System => 2
510    }
511}
512
513/// Filters a list of dependents to find those that are NOT part of the current
514/// removal set.
515fn collect_external_dependents(
516    removal_ids: &std::collections::HashSet<String>,
517    dependents: Vec<String>
518) -> Vec<String> {
519    let mut external = dependents
520        .into_iter()
521        .filter(|dep| !removal_ids.contains(dep))
522        .collect::<Vec<_>>();
523    external.sort();
524    external
525}
526
527/// Resolves a package source string and adds the matching installed manifest to
528/// the removal list.
529///
530/// # Errors
531///
532/// Returns an error if the package name is invalid or if the package is not
533/// installed.
534fn resolve_and_add_manifest(
535    name: &str,
536    installed_packages: &[types::InstallManifest],
537    manifests_to_uninstall: &mut Vec<types::InstallManifest>,
538    scope_override: Option<types::Scope>,
539    yes: bool
540) -> Result<(), String> {
541    let request = match pkg::resolve::parse_source_string(name) {
542        Ok(req) => req,
543        Err(e) => {
544            return Err(format!("Error: Invalid package name '{name}': {e}"));
545        }
546    };
547
548    let mut candidates: Vec<_> = installed_packages
549        .iter()
550        .filter(|m| {
551            let name_matches = m.name == request.name;
552            let sub_matches = m.sub_package == request.sub_package;
553            let scope_matches =
554                scope_override.is_none_or(|scope| m.scope == scope);
555            name_matches && sub_matches && scope_matches
556        })
557        .collect();
558
559    if let Some(repo) = &request.repo {
560        candidates.retain(|m| m.repo == *repo);
561    }
562    if let Some(handle) = &request.handle {
563        candidates.retain(|m| m.registry_handle == *handle);
564    }
565
566    match candidates.len() {
567        0 => Err(format!("Error: Package '{name}' is not installed.")),
568        1 => {
569            if let Some(first) = candidates.first()
570                && !manifests_to_uninstall.iter().any(|m| {
571                    m.name == first.name
572                        && m.sub_package == first.sub_package
573                        && m.repo == first.repo
574                        && m.registry_handle == first.registry_handle
575                })
576            {
577                manifests_to_uninstall.push((*first).clone());
578            }
579            Ok(())
580        }
581        _ => {
582            let owned_candidates =
583                candidates.into_iter().cloned().collect::<Vec<_>>();
584            let chosen =
585                crate::cmd::installed_select::choose_installed_manifest(
586                    name,
587                    &owned_candidates,
588                    yes
589                )
590                .map_err(|e| format!("Error: {e}"))?;
591
592            if !manifests_to_uninstall.iter().any(|m| {
593                m.name == chosen.name
594                    && m.sub_package == chosen.sub_package
595                    && m.repo == chosen.repo
596                    && m.registry_handle == chosen.registry_handle
597                    && m.scope == chosen.scope
598            }) {
599                manifests_to_uninstall.push(chosen);
600            }
601            Ok(())
602        }
603    }
604}
605
606/// Recursively finds dependency-reason packages that would become orphaned if
607/// the target packages were removed.
608///
609/// # Errors
610///
611/// Returns an error if package dependencies cannot be parsed.
612fn collect_recursive_uninstalls(
613    manifests_to_uninstall: &mut Vec<types::InstallManifest>,
614    installed_packages: &[types::InstallManifest]
615) -> Result<()> {
616    let mut changed = true;
617    while changed {
618        changed = false;
619        let mut new_to_add = Vec::new();
620
621        for manifest in manifests_to_uninstall.iter() {
622            for dep_str in &manifest.installed_dependencies {
623                if let Ok(dep) =
624                    pkg::dependencies::parse_dependency_string(dep_str)
625                    && dep.manager == "zoi"
626                {
627                    let Ok(dep_req) =
628                        pkg::resolve::parse_source_string(dep.package)
629                    else {
630                        continue;
631                    };
632
633                    let matching_dep_manifests = installed_packages
634                        .iter()
635                        .filter(|m| {
636                            m.name == dep_req.name
637                                && m.sub_package == dep_req.sub_package
638                                && dep_req
639                                    .repo
640                                    .as_ref()
641                                    .is_none_or(|repo| m.repo == *repo)
642                                && dep_req.handle.as_ref().is_none_or(
643                                    |handle| m.registry_handle == *handle
644                                )
645                                && dep_req
646                                    .version_spec
647                                    .as_ref()
648                                    .is_none_or(|version| m.version == *version)
649                        })
650                        .collect::<Vec<_>>();
651
652                    if let [dm] = matching_dep_manifests.as_slice() {
653                        if !matches!(
654                            dm.reason,
655                            types::InstallReason::Dependency { .. }
656                        ) {
657                            continue;
658                        }
659
660                        if manifests_to_uninstall.iter().any(|m| {
661                            m.name == dm.name && m.sub_package == dm.sub_package
662                        }) || new_to_add.iter().any(
663                            |m: &&types::InstallManifest| {
664                                m.name == dm.name
665                                    && m.sub_package == dm.sub_package
666                            }
667                        ) {
668                            continue;
669                        }
670
671                        let pkg_dir = pkg::local::get_package_dir(
672                            dm.scope,
673                            &dm.registry_handle,
674                            &dm.repo,
675                            &dm.name
676                        )?;
677                        let dependents_from_store =
678                            pkg::local::get_dependents(&pkg_dir)?;
679                        let dependents_from_db = pkg::db::get_dependents(
680                            &dm.registry_handle,
681                            &dm.name,
682                            dm.sub_package.as_deref()
683                        )?;
684
685                        let mut all_dependents = HashSet::new();
686                        for dep_id in dependents_from_store {
687                            all_dependents.insert(dep_id);
688                        }
689                        for dep_pkg in dependents_from_db {
690                            let dep_id = if let Some(sub) = &dep_pkg.sub_package
691                            {
692                                format!(
693                                    "#{}@{}/{}@{}:{}",
694                                    dep_pkg
695                                        .registry_handle
696                                        .as_deref()
697                                        .unwrap_or("local"),
698                                    dep_pkg.repo,
699                                    dep_pkg.name,
700                                    dep_pkg
701                                        .version
702                                        .as_deref()
703                                        .unwrap_or("unknown"),
704                                    sub
705                                )
706                            } else {
707                                format!(
708                                    "#{}@{}/{}@{}",
709                                    dep_pkg
710                                        .registry_handle
711                                        .as_deref()
712                                        .unwrap_or("local"),
713                                    dep_pkg.repo,
714                                    dep_pkg.name,
715                                    dep_pkg
716                                        .version
717                                        .as_deref()
718                                        .unwrap_or("unknown")
719                                )
720                            };
721                            all_dependents.insert(dep_id);
722                        }
723
724                        let all_dependents_will_be_removed =
725                            all_dependents.iter().all(|dep_id| {
726                                manifests_to_uninstall.iter().any(|m| {
727                                    let m_id_short =
728                                        if let Some(sub) = &m.sub_package {
729                                            format!(
730                                                "{}@{}:{}",
731                                                m.name, m.version, sub
732                                            )
733                                        } else {
734                                            format!("{}@{}", m.name, m.version)
735                                        };
736                                    let m_id_full =
737                                        if let Some(sub) = &m.sub_package {
738                                            format!(
739                                                "#{}@{}/{}@{}:{}",
740                                                m.registry_handle,
741                                                m.repo,
742                                                m.name,
743                                                m.version,
744                                                sub
745                                            )
746                                        } else {
747                                            format!(
748                                                "#{}@{}/{}@{}",
749                                                m.registry_handle,
750                                                m.repo,
751                                                m.name,
752                                                m.version
753                                            )
754                                        };
755                                    *dep_id == m_id_short
756                                        || *dep_id == m_id_full
757                                })
758                            });
759
760                        if all_dependents_will_be_removed {
761                            new_to_add.push(dm);
762                            changed = true;
763                        }
764                    }
765                }
766            }
767        }
768
769        for nm in new_to_add {
770            manifests_to_uninstall.push(nm.clone());
771        }
772    }
773    Ok(())
774}
775
776#[cfg(test)]
777mod tests {
778    use std::collections::HashSet;
779
780    use super::collect_external_dependents;
781
782    #[test]
783    fn dangerous_removal_ignores_dependents_in_same_removal_set() {
784        let mut removal_ids = HashSet::new();
785        removal_ids.insert("foo@1.0.0".to_string());
786
787        let external = collect_external_dependents(
788            &removal_ids,
789            vec![
790                "foo@1.0.0".to_string(),
791                "bar@2.0.0".to_string(),
792                "baz@3.0.0".to_string(),
793            ]
794        );
795
796        assert_eq!(
797            external,
798            vec!["bar@2.0.0".to_string(), "baz@3.0.0".to_string()]
799        );
800    }
801
802    #[test]
803    fn dangerous_removal_dependents_are_sorted_for_stable_output() {
804        let removal_ids = HashSet::new();
805        let external = collect_external_dependents(
806            &removal_ids,
807            vec!["c@1".to_string(), "a@1".to_string(), "b@1".to_string()]
808        );
809        assert_eq!(
810            external,
811            vec!["a@1".to_string(), "b@1".to_string(), "c@1".to_string()]
812        );
813    }
814}