Remove an installed extension. Args: extension_id: Extension ID keep_config: If True, preserve config files (don't delete extension dir) Returns: True if extension was removed
(self, extension_id: str, keep_config: bool = False)
| 1531 | ) |
| 1532 | |
| 1533 | def remove(self, extension_id: str, keep_config: bool = False) -> bool: |
| 1534 | """Remove an installed extension. |
| 1535 | |
| 1536 | Args: |
| 1537 | extension_id: Extension ID |
| 1538 | keep_config: If True, preserve config files (don't delete extension dir) |
| 1539 | |
| 1540 | Returns: |
| 1541 | True if extension was removed |
| 1542 | """ |
| 1543 | if not self.registry.is_installed(extension_id): |
| 1544 | return False |
| 1545 | |
| 1546 | # Get registered commands and skills before removal |
| 1547 | metadata = self.registry.get(extension_id) |
| 1548 | registered_commands = ( |
| 1549 | metadata.get("registered_commands", {}) if metadata else {} |
| 1550 | ) |
| 1551 | raw_skills = metadata.get("registered_skills", []) if metadata else [] |
| 1552 | # Normalize: must be a list of plain strings to avoid corrupted-registry errors |
| 1553 | if isinstance(raw_skills, list): |
| 1554 | registered_skills = [s for s in raw_skills if isinstance(s, str)] |
| 1555 | else: |
| 1556 | registered_skills = [] |
| 1557 | |
| 1558 | extension_dir = self.extensions_dir / extension_id |
| 1559 | |
| 1560 | # Unregister commands from all AI agents |
| 1561 | if registered_commands: |
| 1562 | registrar = CommandRegistrar() |
| 1563 | registrar.unregister_commands(registered_commands, self.project_root) |
| 1564 | |
| 1565 | # Unregister agent skills |
| 1566 | self._unregister_extension_skills(registered_skills, extension_id) |
| 1567 | |
| 1568 | if keep_config: |
| 1569 | # Preserve config files, only remove non-config files |
| 1570 | if extension_dir.exists(): |
| 1571 | for child in extension_dir.iterdir(): |
| 1572 | # Keep top-level *-config.yml and *-config.local.yml files |
| 1573 | if child.is_file() and ( |
| 1574 | child.name.endswith("-config.yml") |
| 1575 | or child.name.endswith("-config.local.yml") |
| 1576 | ): |
| 1577 | continue |
| 1578 | if child.is_dir(): |
| 1579 | shutil.rmtree(child) |
| 1580 | else: |
| 1581 | child.unlink() |
| 1582 | else: |
| 1583 | # Backup config files before deleting |
| 1584 | if extension_dir.exists(): |
| 1585 | # Use subdirectory per extension to avoid name accumulation |
| 1586 | # (e.g., jira-jira-config.yml on repeated remove/install cycles) |
| 1587 | backup_dir = self.extensions_dir / ".backup" / extension_id |
| 1588 | backup_dir.mkdir(parents=True, exist_ok=True) |
| 1589 | |
| 1590 | # Backup both primary and local override config files |