def plan_removal(config: ProjectConfig | None, scope: str, selectors: list[str]) -> RemovalPlan:
"""Choose managed installations to remove without changing project declarations."""
if scope not in ("local", "global"):
raise ValueError("Choose --local or --global")
if scope == "global" and "*" in selectors:
raise ValueError("Global removal requires explicit package or version selectors")
root = scope_path(config, scope)
items = installed(root)
try:
catalog = load_catalog(offline=True)
except (ValueError, OSError):
catalog = None
selected: set[tuple[str, str]] = set()
declarations: set[str] = set()
if "*" in selectors:
if len(selectors) != 1:
raise ValueError("'*' cannot be combined with other selectors")
protected: set[tuple[str, str]] = set()
if config and config.mode == "local":
for raw in config.packages:
spec = parse_spec(raw)
name = resolve_installed_name(spec.name, items, catalog)
package = catalog["packages"].get(name, {}) if catalog else {}
try:
matches = usable_installed(Spec(name, spec.operator, spec.version), package, root, items)
except ValueError:
matches = []
if matches:
chosen = max(matches, key=lambda item: item["version_code"])
protected.add((name, chosen["version"]))
selected = {(item["name"], item["version"]) for item in items} - protected
else:
for raw in selectors:
spec = parse_spec(raw)
name = resolve_installed_name(spec.name, items, catalog)
package = catalog["packages"].get(name, {}) if catalog else {}
if spec.operator is None:
selected.update((item["name"], item["version"]) for item in items if item["name"] == name)
if scope == "local" and config and config.mode == "local":
declarations.update(raw for raw in config.packages if resolve_installed_name(parse_spec(raw).name, items, catalog) == name)
else:
matches = matching_installed(Spec(name, spec.operator, spec.version), package, root, items)
selected.update((item["name"], item["version"]) for item in matches)
targets = tuple(item for item in items if (item["name"], item["version"]) in selected)
for item in targets:
managed_install_directory(root, item)
return RemovalPlan(root, targets, tuple(raw for raw in config.packages if raw in declarations) if config else (), config)