Skip to content

Commit 2e5bc22

Browse files
committed
Allow manifest-only uninstall for unknown/removed integrations
- Uninstall no longer requires the integration to be in the registry; falls back to manifest.uninstall() directly when get_integration() returns None - Switch Phase 1 similarly uses manifest-only uninstall for unknown integrations instead of skipping teardown, preventing orphaned files
1 parent 3fdfa00 commit 2e5bc22

1 file changed

Lines changed: 15 additions & 10 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,9 +1766,6 @@ def integration_uninstall(
17661766
raise typer.Exit(1)
17671767

17681768
integration = get_integration(key)
1769-
if integration is None:
1770-
console.print(f"[red]Error:[/red] Unknown integration '{key}'")
1771-
raise typer.Exit(1)
17721769

17731770
manifest_path = project_root / ".specify" / "integrations" / f"{key}.manifest.json"
17741771
if not manifest_path.exists():
@@ -1796,7 +1793,7 @@ def integration_uninstall(
17961793
console.print(f"[dim]Details:[/dim] {exc}")
17971794
raise typer.Exit(1)
17981795

1799-
removed, skipped = integration.teardown(project_root, manifest, force=force)
1796+
removed, skipped = manifest.uninstall(project_root, force=force)
18001797

18011798
_remove_integration_json(project_root)
18021799

@@ -1808,7 +1805,7 @@ def integration_uninstall(
18081805
opts.pop("ai_skills", None)
18091806
save_init_options(project_root, opts)
18101807

1811-
name = (integration.config or {}).get("name", key)
1808+
name = (integration.config or {}).get("name", key) if integration else key
18121809
console.print(f"\n[green]✓[/green] Integration '{name}' uninstalled")
18131810
if removed:
18141811
console.print(f" Removed {len(removed)} file(s)")
@@ -1871,15 +1868,23 @@ def integration_switch(
18711868
f"run [cyan]specify integration uninstall {installed_key}[/cyan], then retry."
18721869
)
18731870
raise typer.Exit(1)
1874-
removed, skipped = current_integration.teardown(
1875-
project_root, old_manifest, force=force,
1876-
)
1871+
removed, skipped = old_manifest.uninstall(project_root, force=force)
18771872
if removed:
18781873
console.print(f" Removed {len(removed)} file(s)")
18791874
if skipped:
18801875
console.print(f" [yellow]⚠[/yellow] {len(skipped)} modified file(s) preserved")
1881-
elif not current_integration:
1882-
console.print(f"[dim]Unknown installed integration '{installed_key}' — skipping uninstall phase[/dim]")
1876+
elif not current_integration and manifest_path.exists():
1877+
# Integration removed from registry but manifest exists — use manifest-only uninstall
1878+
console.print(f"Uninstalling unknown integration '{installed_key}' via manifest")
1879+
try:
1880+
old_manifest = IntegrationManifest.load(installed_key, project_root)
1881+
removed, skipped = old_manifest.uninstall(project_root, force=force)
1882+
if removed:
1883+
console.print(f" Removed {len(removed)} file(s)")
1884+
if skipped:
1885+
console.print(f" [yellow]⚠[/yellow] {len(skipped)} modified file(s) preserved")
1886+
except (ValueError, FileNotFoundError) as exc:
1887+
console.print(f"[yellow]Warning:[/yellow] Could not read manifest for '{installed_key}': {exc}")
18831888
else:
18841889
console.print(f"[dim]No manifest for '{installed_key}' — skipping uninstall phase[/dim]")
18851890

0 commit comments

Comments
 (0)