Skip to content
Draft
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: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ uv tool install skillup
skillup add google/gemini-cli-skills
```

Need to use your OS trust store for TLS?

```bash
skillup --system-certs add google/gemini-cli-skills
```

No releases? Falls back to `main` automatically. Pin a branch explicitly:

```bash
Expand Down
6 changes: 6 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All commands accept a `--global` / `-g` flag that switches the lock file and bas
skillup --global add myorg/skills
```

All commands also accept `--system-certs` to use the system certificate store for HTTPS requests.

```bash
skillup --system-certs add myorg/skills
```

---

## `add`
Expand Down
2 changes: 2 additions & 0 deletions skillup/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
@app.callback()
def main(
is_global: bool = typer.Option(False, "--global", "-g", help="Use home directory instead of current directory"),
system_certs: bool = typer.Option(False, "--system-certs", help="Use system certificate store for HTTPS requests"),
):
"""Minimal CLI to manage agent skills from GitHub releases or branches."""
settings.is_global = is_global
settings.use_system_certs = system_certs


@app.command()
Expand Down
4 changes: 2 additions & 2 deletions skillup/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ def get_github_headers() -> dict[str, str]:

def get_latest_release(repo: str) -> tuple[str, str]:
url = f"https://api.github.com/repos/{repo}/releases/latest"
response = requests.get(url, headers=get_github_headers())
response = requests.get(url, headers=get_github_headers(), verify=settings.tls_verify)
response.raise_for_status()
data = response.json()
return data["tag_name"], data["zipball_url"]


def get_commit_sha(repo: str, ref: str) -> str:
url = f"https://api.github.com/repos/{repo}/commits/{ref}"
response = requests.get(url, headers=get_github_headers())
response = requests.get(url, headers=get_github_headers(), verify=settings.tls_verify)
response.raise_for_status()
data = response.json()
return data["sha"]
Expand Down
2 changes: 1 addition & 1 deletion skillup/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def download_release(repo: str, version: str, url: str) -> Path:
) as progress:
progress.add_task(description=f"Downloading {repo} {version}...", total=None)

response = requests.get(url, headers=get_github_headers(), stream=True)
response = requests.get(url, headers=get_github_headers(), stream=True, verify=settings.tls_verify)
response.raise_for_status()
with open(cache_path, "wb") as f:
shutil.copyfileobj(response.raw, f)
Expand Down
13 changes: 13 additions & 0 deletions skillup/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import ssl
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
Expand Down Expand Up @@ -26,6 +27,7 @@ def format_source_label(source: RepoSource) -> str:
@dataclass
class Settings:
is_global: bool = False
use_system_certs: bool = False

@property
def base_dir(self) -> Path:
Expand Down Expand Up @@ -54,5 +56,16 @@ def cache_dir(self) -> Path:
def lock_file(self) -> Path:
return self.agents_dir / "skills.lock.json"

@property
def tls_verify(self) -> bool | str:
if not self.use_system_certs:
return True
verify_paths = ssl.get_default_verify_paths()
if verify_paths.cafile:
return verify_paths.cafile
if verify_paths.capath:
return verify_paths.capath
return True


settings = Settings()
9 changes: 9 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from skillup.cli import app
from skillup.settings import settings
from typer.testing import CliRunner

runner = CliRunner()
Expand All @@ -10,3 +11,11 @@ def test_help():
assert "remove" in result.stdout
assert "update" in result.stdout
assert "sync" in result.stdout


def test_system_certs_flag_sets_setting():
settings.use_system_certs = False
result = runner.invoke(app, ["--system-certs", "sync"])
assert result.exit_code == 0
assert settings.use_system_certs is True
settings.use_system_certs = False
1 change: 1 addition & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def temp_dirs(tmp_path):
patch("os.getenv", side_effect=lambda key, default=None: str(fake_temp) if key == "TEMP" else default):
# Reset settings to default before each test
settings.is_global = False
settings.use_system_certs = False
yield fake_home, fake_cwd

@pytest.fixture
Expand Down
24 changes: 24 additions & 0 deletions tests/test_github_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
get_repo_source,
)
from skillup.install import download_release
from skillup.settings import settings

def test_get_github_token_from_env():
with patch.dict(os.environ, {"GITHUB_TOKEN": "env_token"}):
Expand Down Expand Up @@ -40,6 +41,7 @@ def test_get_github_token_not_found():
@patch("skillup.github.get_github_token")
@patch("requests.get")
def test_get_latest_release_uses_token(mock_get, mock_token):
settings.use_system_certs = False
mock_token.return_value = "test_token"
mock_get.return_value.json.return_value = {"tag_name": "v1.0.0", "zipball_url": "url"}
mock_get.return_value.raise_for_status = MagicMock()
Expand All @@ -48,10 +50,12 @@ def test_get_latest_release_uses_token(mock_get, mock_token):

args, kwargs = mock_get.call_args
assert kwargs["headers"]["Authorization"] == "token test_token"
assert kwargs["verify"] is True

@patch("skillup.github.get_github_token")
@patch("requests.get")
def test_get_commit_sha_uses_token(mock_get, mock_token):
settings.use_system_certs = False
mock_token.return_value = "test_token"
mock_get.return_value.json.return_value = {"sha": "abc123"}
mock_get.return_value.raise_for_status = MagicMock()
Expand All @@ -60,6 +64,24 @@ def test_get_commit_sha_uses_token(mock_get, mock_token):

args, kwargs = mock_get.call_args
assert kwargs["headers"]["Authorization"] == "token test_token"
assert kwargs["verify"] is True


@patch("skillup.github.get_github_token")
@patch("requests.get")
def test_get_latest_release_uses_system_certs(mock_get, mock_token):
settings.use_system_certs = True
with patch("skillup.settings.ssl.get_default_verify_paths") as mock_verify_paths:
mock_verify_paths.return_value = MagicMock(cafile="/etc/ssl/certs/custom.pem", capath=None)
mock_token.return_value = "test_token"
mock_get.return_value.json.return_value = {"tag_name": "v1.0.0", "zipball_url": "url"}
mock_get.return_value.raise_for_status = MagicMock()

get_latest_release("owner/repo")

args, kwargs = mock_get.call_args
assert kwargs["verify"] == "/etc/ssl/certs/custom.pem"
settings.use_system_certs = False

@patch("skillup.github.get_commit_sha")
@patch("skillup.github.get_latest_release")
Expand All @@ -80,6 +102,7 @@ def test_get_repo_source_falls_back_to_main_branch(mock_latest, mock_commit):
@patch("shutil.copyfileobj")
@patch("builtins.open", new_callable=MagicMock)
def test_download_release_uses_token(mock_open, mock_copy, mock_get, mock_token, tmp_path):
settings.use_system_certs = False
mock_token.return_value = "test_token"
mock_get.return_value.raw = MagicMock()
mock_get.return_value.raise_for_status = MagicMock()
Expand All @@ -90,3 +113,4 @@ def test_download_release_uses_token(mock_open, mock_copy, mock_get, mock_token,

args, kwargs = mock_get.call_args
assert kwargs["headers"]["Authorization"] == "token test_token"
assert kwargs["verify"] is True
1 change: 1 addition & 0 deletions tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def temp_dirs(tmp_path):
patch("pathlib.Path.cwd", return_value=fake_cwd), \
patch("os.getenv", side_effect=lambda key, default=None: str(fake_temp) if key == "TEMP" else default):
settings.is_global = False
settings.use_system_certs = False
yield fake_home, fake_cwd


Expand Down
1 change: 1 addition & 0 deletions tests/test_migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def temp_dirs(tmp_path):
patch("pathlib.Path.cwd", return_value=fake_cwd), \
patch("os.getenv", side_effect=lambda key, default=None: str(fake_temp) if key == "TEMP" else default):
settings.is_global = False
settings.use_system_certs = False
yield fake_home, fake_cwd


Expand Down
1 change: 1 addition & 0 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def temp_dirs(tmp_path):
patch("pathlib.Path.cwd", return_value=fake_cwd), \
patch("os.getenv", side_effect=lambda key, default=None: str(fake_temp) if key == "TEMP" else default):
settings.is_global = False
settings.use_system_certs = False
yield fake_home, fake_cwd


Expand Down