Install preset from ZIP file. Args: zip_path: Path to preset ZIP file speckit_version: Current spec-kit version priority: Resolution priority (lower = higher precedence, default 10) Returns: Installed preset manifest Raises:
(
self,
zip_path: Path,
speckit_version: str,
priority: int = 10,
)
| 1618 | return manifest |
| 1619 | |
| 1620 | def install_from_zip( |
| 1621 | self, |
| 1622 | zip_path: Path, |
| 1623 | speckit_version: str, |
| 1624 | priority: int = 10, |
| 1625 | ) -> PresetManifest: |
| 1626 | """Install preset from ZIP file. |
| 1627 | |
| 1628 | Args: |
| 1629 | zip_path: Path to preset ZIP file |
| 1630 | speckit_version: Current spec-kit version |
| 1631 | priority: Resolution priority (lower = higher precedence, default 10) |
| 1632 | |
| 1633 | Returns: |
| 1634 | Installed preset manifest |
| 1635 | |
| 1636 | Raises: |
| 1637 | PresetValidationError: If manifest is invalid or priority is invalid |
| 1638 | PresetCompatibilityError: If pack is incompatible |
| 1639 | """ |
| 1640 | # Validate priority early |
| 1641 | if priority < 1: |
| 1642 | raise PresetValidationError("Priority must be a positive integer (1 or higher)") |
| 1643 | |
| 1644 | with tempfile.TemporaryDirectory() as tmpdir: |
| 1645 | temp_path = Path(tmpdir) |
| 1646 | |
| 1647 | with zipfile.ZipFile(zip_path, 'r') as zf: |
| 1648 | temp_path_resolved = temp_path.resolve() |
| 1649 | for member in zf.namelist(): |
| 1650 | member_path = (temp_path / member).resolve() |
| 1651 | try: |
| 1652 | member_path.relative_to(temp_path_resolved) |
| 1653 | except ValueError: |
| 1654 | raise PresetValidationError( |
| 1655 | f"Unsafe path in ZIP archive: {member} " |
| 1656 | "(potential path traversal)" |
| 1657 | ) |
| 1658 | zf.extractall(temp_path) |
| 1659 | |
| 1660 | pack_dir = temp_path |
| 1661 | manifest_path = pack_dir / "preset.yml" |
| 1662 | |
| 1663 | if not manifest_path.exists(): |
| 1664 | subdirs = [d for d in temp_path.iterdir() if d.is_dir()] |
| 1665 | if len(subdirs) == 1: |
| 1666 | pack_dir = subdirs[0] |
| 1667 | manifest_path = pack_dir / "preset.yml" |
| 1668 | |
| 1669 | if not manifest_path.exists(): |
| 1670 | raise PresetValidationError( |
| 1671 | "No preset.yml found in ZIP file" |
| 1672 | ) |
| 1673 | |
| 1674 | return self.install_from_directory(pack_dir, speckit_version, priority) |
| 1675 | |
| 1676 | def remove(self, pack_id: str) -> bool: |
| 1677 | """Remove an installed preset. |