Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- uses: arduino/setup-task@v2
with:
version: 3.x

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.13"

Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,23 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.13"

- run: uv build

- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v4
with:
subject-path: dist/*

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
Expand All @@ -58,7 +58,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
Expand All @@ -75,7 +75,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
Expand Down
127 changes: 88 additions & 39 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -1,39 +1,88 @@
name: Validate

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
with:
scandir: .devcontainer/scripts
severity: warning

compose:
name: Compose Config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate compose config
run: |
cp .devcontainer/.env.example .devcontainer/.env
docker compose -f .devcontainer/compose.yaml config --quiet

build:
name: Devcontainer Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build devcontainer
uses: devcontainers/ci@v0.3
with:
runCmd: echo "devcontainer smoke test passed"
name: Validate

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
devcontainer: ${{ steps.filter.outputs.devcontainer }}
scripts: ${{ steps.filter.outputs.scripts }}
compose: ${{ steps.filter.outputs.compose }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
devcontainer:
- '.devcontainer/devcontainer.json'
- '.devcontainer/compose.yaml'
- '.devcontainer/compose/**'
- '.devcontainer/config/**'
- '.devcontainer/scripts/**'
- '.devcontainer/.env.example'
scripts:
- '.devcontainer/scripts/**'
compose:
- '.devcontainer/compose.yaml'
- '.devcontainer/compose/**'
- '.devcontainer/.env.example'

shellcheck:
name: ShellCheck
needs: changes
if: needs.changes.outputs.scripts == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
with:
scandir: .devcontainer/scripts
severity: warning

compose:
name: Compose Config
needs: changes
if: needs.changes.outputs.compose == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Validate compose config
run: |
cp .devcontainer/.env.example .devcontainer/.env
docker compose -f .devcontainer/compose.yaml config --quiet

build:
name: Devcontainer Build
needs: changes
if: needs.changes.outputs.devcontainer == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build devcontainer
uses: devcontainers/ci@v0.3
with:
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
runCmd: echo "devcontainer smoke test passed"

# Always-pass job for branch protection required checks
validate-status:
name: Validate Status
if: always()
needs: [shellcheck, compose, build]
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more validation jobs failed"
exit 1
fi
echo "All validation checks passed (or were skipped due to no relevant changes)"
80 changes: 80 additions & 0 deletions src/musher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"""Musher Python SDK — programmatic access to the Musher bundle registry."""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version as _metadata_version
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pathlib import Path

from musher._auth import resolve_registry_url
from musher._bundle import Asset, Bundle, Manifest, ManifestAsset, ResolveResult
from musher._cache import BundleCache
from musher._cache_info import CachedBundle, CachedBundleVersion, CacheInfo
from musher._client import AsyncClient, Client
from musher._config import MusherConfig, configure, get_config
from musher._errors import (
Expand Down Expand Up @@ -54,6 +62,9 @@
"BundleVersionState",
"BundleVisibility",
"CacheError",
"CacheInfo",
"CachedBundle",
"CachedBundleVersion",
"ClaudePluginExport",
"Client",
"FileHandle",
Expand All @@ -72,6 +83,11 @@
"ToolsetHandle",
"VersionNotFoundError",
"__version__",
"cache_clean",
"cache_clear",
"cache_info",
"cache_path",
"cache_remove",
"configure",
"get_config",
"pull",
Expand Down Expand Up @@ -104,3 +120,67 @@ async def resolve_async(ref: str) -> ResolveResult:
"""Resolve a bundle reference without pulling (async convenience)."""
async with AsyncClient() as client:
return await client.resolve(ref)


# ── Cache management ──────────────────────────────────────────────


def _make_cache(
cache_dir: Path | None = None,
registry_url: str | None = None,
) -> BundleCache:
from musher._paths import cache_dir as _default_cache_dir # noqa: PLC0415

return BundleCache(
cache_dir=cache_dir or _default_cache_dir(),
registry_url=registry_url or resolve_registry_url(),
)


def cache_info(
*,
cache_dir: Path | None = None,
registry_url: str | None = None,
) -> CacheInfo:
"""Scan the local bundle cache and return statistics."""
return _make_cache(cache_dir, registry_url).scan()


def cache_remove(
ref: str,
*,
cache_dir: Path | None = None,
registry_url: str | None = None,
) -> None:
"""Remove a specific bundle from the cache."""
parsed = BundleRef.parse(ref)
_make_cache(cache_dir, registry_url).purge(parsed.namespace, parsed.slug, parsed.version)


def cache_clear(*, cache_dir: Path | None = None) -> None:
"""Remove all cached data."""
from musher._paths import cache_dir as _default_cache_dir # noqa: PLC0415

cache = BundleCache(cache_dir=cache_dir or _default_cache_dir())
cache.clear()


def cache_clean(
*,
cache_dir: Path | None = None,
registry_url: str | None = None,
) -> int:
"""Remove expired entries and garbage-collect orphaned blobs.

Returns the number of entries removed.
"""
return _make_cache(cache_dir, registry_url).clean()


def cache_path(*, cache_dir: Path | None = None) -> Path:
"""Return the cache directory path."""
if cache_dir:
return cache_dir
from musher._paths import cache_dir as _default_cache_dir # noqa: PLC0415

return _default_cache_dir()
Loading
Loading