Commit 58ce653
feat(extensions): Quality of life improvements for RFC-aligned catalog integration (#1776)
* feat(extensions): implement automatic updates with atomic backup/restore
- Implement automatic extension updates with download from catalog
- Add comprehensive backup/restore mechanism for failed updates:
- Backup registry entry before update
- Backup extension directory
- Backup command files for all AI agents
- Backup hooks from extensions.yml
- Add extension ID verification after install
- Add KeyboardInterrupt handling to allow clean cancellation
- Fix enable/disable to preserve installed_at timestamp by using
direct registry manipulation instead of registry.add()
- Add rollback on any update failure with command file,
hook, and registry restoration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): comprehensive name resolution and error handling improvements
- Add shared _resolve_installed_extension helper for ID/display name resolution
with proper ambiguous name handling (shows table of matches)
- Add _resolve_catalog_extension helper for catalog lookups by ID or display name
- Update enable/disable/update/remove commands to use name resolution helpers
- Fix extension_info to handle catalog errors gracefully:
- Fallback to local installed info when catalog unavailable
- Distinguish "catalog unavailable" from "not found in catalog"
- Support display name lookup for both installed and catalog extensions
- Use resolved display names in all status messages for consistency
- Extract _print_extension_info helper for DRY catalog info printing
Addresses reviewer feedback:
- Ambiguous name handling in enable/disable/update
- Catalog error fallback for installed extensions
- UX message clarity (catalog unavailable vs not found)
- Resolved ID in status messages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): properly detect ambiguous names in extension_info
The extension_info command was breaking on the first name match without
checking for ambiguity. This fix separates ID matching from name matching
and checks for ambiguity before selecting a match, consistent with the
_resolve_installed_extension() helper used by other commands.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(extensions): add public update() method to ExtensionRegistry
Add a proper public API for updating registry metadata while preserving
installed_at timestamp, instead of directly mutating internal registry
data and calling private _save() method.
Changes:
- Add ExtensionRegistry.update() method that preserves installed_at
- Update enable/disable commands to use registry.update()
- Update rollback logic to use registry.update()
This decouples the CLI from registry internals and maintains proper
encapsulation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): safely access optional author field in extension_info
ExtensionManifest doesn't expose an author property - the author field
is optional in extension.yml and stored in data["extension"]["author"].
Use safe dict access to avoid AttributeError.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): address multiple reviewer comments
- ExtensionRegistry.update() now preserves original installed_at timestamp
- Add ExtensionRegistry.restore() for rollback (entry was removed)
- Clean up wrongly installed extension on ID mismatch before rollback
- Remove unused catalog_error parameter from _print_extension_info()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): check _install_allowed for updates, preserve backup on failed rollback
- Skip automatic updates for extensions from catalogs with install_allowed=false
- Only delete backup directory on successful rollback, preserve it on failure
for manual recovery
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): address reviewer feedback on update/rollback logic
- Hook rollback: handle empty backup_hooks by checking `is not None`
instead of truthiness (falsy empty dict would skip hook cleanup)
- extension_info: use resolved_installed_id for catalog lookup when
extension was found by display name (prevents wrong catalog match)
- Rollback: always remove extension dir first, then restore if backup
exists (handles case when no original dir existed before update)
- Validate extension ID from ZIP before installing, not after
(avoids side effects of installing wrong extension before rollback)
- Preserve enabled state during updates: re-apply disabled state and
hook enabled flags after successful update
- Optimize _resolve_catalog_extension: pass query to catalog.search()
instead of fetching all extensions
- update() now merges metadata with existing entry instead of replacing
(preserves fields like registered_commands when only updating enabled)
- Add tests for ExtensionRegistry.update() and restore() methods:
- test_update_preserves_installed_at
- test_update_merges_with_existing
- test_update_raises_for_missing_extension
- test_restore_overwrites_completely
- test_restore_can_recreate_removed_entry
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs(extensions): update RFC to reflect implemented status
- Change status from "Draft" to "Implemented"
- Update all Implementation Phases to show completed items
- Add new features implemented beyond original RFC:
- Display name resolution for all commands
- Ambiguous name handling with tables
- Atomic update with rollback
- Pre-install ID validation
- Enabled state preservation
- Registry update/restore methods
- Catalog error fallback
- _install_allowed flag
- Cache invalidation
- Convert Open Questions to Resolved Questions with decisions
- Add remaining Open Questions (sandboxing, signatures) as future work
- Fix table of contents links
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): address third round of PR review comments
- Refactor extension_info to use _resolve_installed_extension() helper
with new allow_not_found parameter instead of duplicating resolution logic
- Fix rollback hook restoration to not create empty hooks: {} in config
when original config had no hooks section
- Fix ZIP pre-validation to handle nested extension.yml files (GitHub
auto-generated ZIPs have structure like repo-name-branch/extension.yml)
- Replace unused installed_manifest variable with _ placeholder
- Add display name to update status messages for better UX
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(extensions): address fourth round of PR review comments
Rollback fixes:
- Preserve installed_at timestamp after successful update (was reset by
install_from_zip calling registry.add)
- Fix rollback to only delete extension_dir if backup exists (avoids
destroying valid installation when failure happens before modification)
- Fix rollback to remove NEW command files created by failed install
(files that weren't in original backup are now cleaned up)
- Fix rollback to delete hooks key entirely when backup_hooks is None
(original config had no hooks key, so restore should remove it)
Cross-command consistency fix:
- Add display name resolution to `extension add` command using
_resolve_catalog_extension() helper (was only in `extension info`)
- Use resolved extension ID for download_extension() call, not original
argument which may be a display name
Security fix (fail-closed):
- Malformed catalog config (empty/missing URLs) now raises ValidationError
instead of silently falling back to built-in catalogs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(lint): address ruff linting errors and registry.update() semantics
- Remove unused import ExtensionError in extension_info
- Remove extraneous f-prefix from strings without placeholders
- Use registry.restore() instead of registry.update() for installed_at
preservation (update() always preserves existing installed_at, ignoring
our override)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: iamaeroplane <michal.bachorik@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>1 parent 82f8a13 commit 58ce653
File tree
4 files changed
+1231
-281
lines changed- extensions
- src/specify_cli
- tests
4 files changed
+1231
-281
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| |||
1504 | 1505 | | |
1505 | 1506 | | |
1506 | 1507 | | |
1507 | | - | |
| 1508 | + | |
1508 | 1509 | | |
1509 | 1510 | | |
1510 | 1511 | | |
1511 | 1512 | | |
1512 | 1513 | | |
1513 | | - | |
1514 | | - | |
1515 | | - | |
1516 | | - | |
1517 | | - | |
1518 | | - | |
1519 | | - | |
1520 | | - | |
1521 | | - | |
1522 | | - | |
| 1514 | + | |
| 1515 | + | |
| 1516 | + | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
| 1522 | + | |
| 1523 | + | |
1523 | 1524 | | |
1524 | 1525 | | |
1525 | 1526 | | |
1526 | | - | |
1527 | | - | |
1528 | | - | |
| 1527 | + | |
| 1528 | + | |
| 1529 | + | |
1529 | 1530 | | |
1530 | | - | |
| 1531 | + | |
1531 | 1532 | | |
1532 | 1533 | | |
1533 | 1534 | | |
1534 | 1535 | | |
1535 | 1536 | | |
1536 | | - | |
1537 | | - | |
1538 | | - | |
1539 | | - | |
1540 | | - | |
1541 | | - | |
1542 | | - | |
1543 | | - | |
1544 | | - | |
1545 | | - | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
1546 | 1547 | | |
1547 | 1548 | | |
1548 | 1549 | | |
1549 | | - | |
1550 | | - | |
1551 | | - | |
1552 | | - | |
| 1550 | + | |
| 1551 | + | |
| 1552 | + | |
| 1553 | + | |
1553 | 1554 | | |
1554 | | - | |
| 1555 | + | |
1555 | 1556 | | |
1556 | 1557 | | |
1557 | 1558 | | |
1558 | 1559 | | |
1559 | 1560 | | |
1560 | | - | |
1561 | | - | |
1562 | | - | |
1563 | | - | |
1564 | | - | |
1565 | | - | |
1566 | | - | |
| 1561 | + | |
| 1562 | + | |
| 1563 | + | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
| 1569 | + | |
| 1570 | + | |
1567 | 1571 | | |
1568 | 1572 | | |
1569 | 1573 | | |
1570 | | - | |
1571 | | - | |
1572 | | - | |
| 1574 | + | |
| 1575 | + | |
| 1576 | + | |
| 1577 | + | |
1573 | 1578 | | |
1574 | | - | |
| 1579 | + | |
1575 | 1580 | | |
1576 | 1581 | | |
1577 | 1582 | | |
1578 | 1583 | | |
1579 | 1584 | | |
1580 | | - | |
1581 | | - | |
1582 | | - | |
1583 | | - | |
1584 | | - | |
1585 | | - | |
1586 | | - | |
1587 | | - | |
1588 | | - | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
1589 | 1606 | | |
1590 | 1607 | | |
1591 | 1608 | | |
1592 | | - | |
1593 | | - | |
1594 | | - | |
| 1609 | + | |
| 1610 | + | |
| 1611 | + | |
| 1612 | + | |
| 1613 | + | |
| 1614 | + | |
1595 | 1615 | | |
1596 | | - | |
| 1616 | + | |
1597 | 1617 | | |
1598 | 1618 | | |
1599 | 1619 | | |
1600 | 1620 | | |
1601 | 1621 | | |
1602 | | - | |
1603 | | - | |
1604 | | - | |
1605 | | - | |
1606 | | - | |
1607 | | - | |
1608 | | - | |
1609 | | - | |
1610 | | - | |
1611 | | - | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
1612 | 1628 | | |
1613 | 1629 | | |
1614 | 1630 | | |
1615 | | - | |
1616 | | - | |
1617 | | - | |
| 1631 | + | |
| 1632 | + | |
1618 | 1633 | | |
1619 | 1634 | | |
1620 | 1635 | | |
1621 | | - | |
| 1636 | + | |
1622 | 1637 | | |
1623 | | - | |
| 1638 | + | |
1624 | 1639 | | |
1625 | | - | |
| 1640 | + | |
1626 | 1641 | | |
1627 | | - | |
| 1642 | + | |
1628 | 1643 | | |
1629 | | - | |
1630 | | - | |
1631 | | - | |
| 1644 | + | |
1632 | 1645 | | |
1633 | | - | |
| 1646 | + | |
1634 | 1647 | | |
1635 | 1648 | | |
1636 | 1649 | | |
1637 | | - | |
| 1650 | + | |
1638 | 1651 | | |
1639 | 1652 | | |
1640 | 1653 | | |
1641 | | - | |
1642 | | - | |
1643 | | - | |
1644 | | - | |
1645 | | - | |
| 1654 | + | |
1646 | 1655 | | |
1647 | | - | |
| 1656 | + | |
1648 | 1657 | | |
1649 | 1658 | | |
1650 | 1659 | | |
1651 | | - | |
| 1660 | + | |
1652 | 1661 | | |
1653 | 1662 | | |
1654 | 1663 | | |
1655 | | - | |
1656 | | - | |
1657 | | - | |
1658 | | - | |
1659 | | - | |
| 1664 | + | |
1660 | 1665 | | |
1661 | | - | |
| 1666 | + | |
1662 | 1667 | | |
1663 | 1668 | | |
1664 | 1669 | | |
1665 | | - | |
| 1670 | + | |
1666 | 1671 | | |
1667 | 1672 | | |
1668 | 1673 | | |
1669 | | - | |
1670 | | - | |
1671 | | - | |
1672 | | - | |
1673 | | - | |
| 1674 | + | |
1674 | 1675 | | |
1675 | | - | |
| 1676 | + | |
1676 | 1677 | | |
1677 | 1678 | | |
1678 | 1679 | | |
1679 | | - | |
| 1680 | + | |
1680 | 1681 | | |
1681 | 1682 | | |
1682 | 1683 | | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
1683 | 1706 | | |
1684 | 1707 | | |
1685 | | - | |
1686 | | - | |
1687 | | - | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
1688 | 1711 | | |
1689 | | - | |
| 1712 | + | |
1690 | 1713 | | |
1691 | 1714 | | |
1692 | 1715 | | |
1693 | | - | |
| 1716 | + | |
1694 | 1717 | | |
1695 | | - | |
| 1718 | + | |
1696 | 1719 | | |
1697 | 1720 | | |
1698 | 1721 | | |
1699 | | - | |
1700 | | - | |
1701 | | - | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
1702 | 1725 | | |
1703 | | - | |
| 1726 | + | |
1704 | 1727 | | |
1705 | 1728 | | |
1706 | 1729 | | |
| |||
0 commit comments