fix(integrations): don't abort uninstall when the manifest can't be deleted (#3805)

`IntegrationManifest.uninstall()` guards every tracked-file `path.unlink()`
with `except OSError: skipped.append(path)`, but the manifest's own
`manifest.unlink()` is bare. The manifest is deleted *last*, so an
undeletable manifest (read-only file, a directory left at the path, a
Windows lock) raises after the tracked files are already gone.

The caller loses the `(removed, skipped)` result and never runs its
post-uninstall bookkeeping — reassigning the default integration,
rewriting/removing `integration.json`, clearing init options — leaving a
removed integration still recorded as installed.

Report it in `skipped` like any other file we could not remove, mirroring
the `path.unlink()` guard above and the same `except OSError:
skipped.append(...)` pattern in kimi's legacy-directory cleanup.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-29 19:28:29 +05:00
committed by GitHub
parent 623466dc42
commit 89126f3a33
2 changed files with 41 additions and 1 deletions

View File

@@ -400,7 +400,19 @@ class IntegrationManifest:
# Remove the manifest file itself
manifest = root / ".specify" / "integrations" / f"{self.key}.manifest.json"
if remove_manifest and manifest.exists():
manifest.unlink()
try:
manifest.unlink()
except OSError:
# An undeletable manifest (read-only file, a directory left at
# the path, a Windows lock) must not abort the uninstall after
# the tracked files were already removed: the caller would lose
# the (removed, skipped) result and never run its post-uninstall
# bookkeeping. Report it like any other file we could not
# remove, mirroring the path.unlink() guard above. The
# empty-parent cleanup below is left unconditional: with the
# manifest still on disk its parent is non-empty, so the first
# rmdir() raises and breaks immediately.
skipped.append(manifest)
parent = manifest.parent
while parent != root:
try: