skillup 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skillup-0.3.2/.github/workflows/ci.yml +25 -0
- skillup-0.3.2/.github/workflows/publish.yml +19 -0
- skillup-0.3.2/.gitignore +10 -0
- skillup-0.3.2/.python-version +1 -0
- skillup-0.3.2/LICENSE +21 -0
- skillup-0.3.2/PKG-INFO +113 -0
- skillup-0.3.2/README.md +101 -0
- skillup-0.3.2/pyproject.toml +31 -0
- skillup-0.3.2/skillup/__init__.py +0 -0
- skillup-0.3.2/skillup/cli.py +260 -0
- skillup-0.3.2/skillup/github.py +89 -0
- skillup-0.3.2/skillup/install.py +82 -0
- skillup-0.3.2/skillup/lock.py +84 -0
- skillup-0.3.2/skillup/settings.py +58 -0
- skillup-0.3.2/tests/test_cli.py +12 -0
- skillup-0.3.2/tests/test_e2e.py +235 -0
- skillup-0.3.2/tests/test_github_auth.py +92 -0
- skillup-0.3.2/tests/test_migrate.py +153 -0
- skillup-0.3.2/tests/test_sync.py +73 -0
- skillup-0.3.2/uv.lock +369 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v3
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
run: uv python install
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: uv sync
|
|
22
|
+
- name: Type check
|
|
23
|
+
run: uv run pyright skillup
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: uv run pytest
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write # Required for trusted publishing
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v3
|
|
16
|
+
- name: Build
|
|
17
|
+
run: uv build
|
|
18
|
+
- name: Publish to PyPI
|
|
19
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
skillup-0.3.2/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
skillup-0.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
skillup-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skillup
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: A minimal CLI to manage agent skills from GitHub releases.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: questionary
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Requires-Dist: typer
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# skillup
|
|
14
|
+
|
|
15
|
+
A minimal, user-friendly Python CLI to manage agent skills from GitHub releases or branches. It installs skills to both `~/.agents/skills` and `~/.claude/skills` for seamless integration across platforms.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Interactive Installation:** Select specific skills to add from any GitHub repository.
|
|
20
|
+
- **Multi-Repo Support:** Manage skills from multiple repositories independently.
|
|
21
|
+
- **Lock File State:** Tracks installed sources (release tags or branches), pinned commit SHAs, and skills in `~/.agents/skills.lock.json` for reproducibility.
|
|
22
|
+
- **Automated Updates:** Easily upgrade all or specific repositories to their latest GitHub release or tracked branch head.
|
|
23
|
+
- **Smart Caching:** Downloads are cached in a temporary directory (`TEMP` or `/tmp`) to avoid redundant network usage. Can be overridden with `SKILLUP_CACHE_DIR`.
|
|
24
|
+
- **GitHub CLI Integration:** Uses the `gh` tool for fast downloads if available, with a reliable `requests` fallback.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install using `pip` or `uv`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install skillup
|
|
32
|
+
# or
|
|
33
|
+
uv tool install skillup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### 1. Add Skills
|
|
39
|
+
Interactively select skills to add from a GitHub repository's latest release:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
skillup add google/gemini-cli-skills
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If a repository has no releases, the CLI automatically falls back to the `main` branch. You can also install directly from a branch:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
skillup add anthropics/skills --branch main --skill pdf
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Remove Skills
|
|
52
|
+
Interactively select installed skills to remove from your system:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
skillup remove
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Update Skills
|
|
59
|
+
Update all installed skills to their latest tracked versions:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
skillup update
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or update a specific repository:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
skillup update --repo google/gemini-cli-skills
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Sync Skills
|
|
72
|
+
Install all skills as defined in the lock file using the pinned commit SHAs (useful for setting up a new machine):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
skillup sync
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. Migrate from NPX Skills CLI
|
|
79
|
+
If you already have a `skills-lock.json` in your repository root, you can import it into the skillup lock format. The latest release or branch commit is resolved from GitHub at migration time.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
skillup migrate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A custom path can be provided if the file is elsewhere:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
skillup migrate path/to/skills-lock.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Skill Definition
|
|
92
|
+
A folder is recognized as a valid skill if it resides within a `skills/` directory at the repository root and contains a `SKILL.md` file.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Install dependencies
|
|
100
|
+
uv sync
|
|
101
|
+
|
|
102
|
+
# Run locally
|
|
103
|
+
uv run skillup --help
|
|
104
|
+
|
|
105
|
+
# Run tests
|
|
106
|
+
uv run pytest
|
|
107
|
+
|
|
108
|
+
# Type check
|
|
109
|
+
uv run pyright skillup
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
MIT
|
skillup-0.3.2/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# skillup
|
|
2
|
+
|
|
3
|
+
A minimal, user-friendly Python CLI to manage agent skills from GitHub releases or branches. It installs skills to both `~/.agents/skills` and `~/.claude/skills` for seamless integration across platforms.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Interactive Installation:** Select specific skills to add from any GitHub repository.
|
|
8
|
+
- **Multi-Repo Support:** Manage skills from multiple repositories independently.
|
|
9
|
+
- **Lock File State:** Tracks installed sources (release tags or branches), pinned commit SHAs, and skills in `~/.agents/skills.lock.json` for reproducibility.
|
|
10
|
+
- **Automated Updates:** Easily upgrade all or specific repositories to their latest GitHub release or tracked branch head.
|
|
11
|
+
- **Smart Caching:** Downloads are cached in a temporary directory (`TEMP` or `/tmp`) to avoid redundant network usage. Can be overridden with `SKILLUP_CACHE_DIR`.
|
|
12
|
+
- **GitHub CLI Integration:** Uses the `gh` tool for fast downloads if available, with a reliable `requests` fallback.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install using `pip` or `uv`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install skillup
|
|
20
|
+
# or
|
|
21
|
+
uv tool install skillup
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### 1. Add Skills
|
|
27
|
+
Interactively select skills to add from a GitHub repository's latest release:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
skillup add google/gemini-cli-skills
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If a repository has no releases, the CLI automatically falls back to the `main` branch. You can also install directly from a branch:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
skillup add anthropics/skills --branch main --skill pdf
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Remove Skills
|
|
40
|
+
Interactively select installed skills to remove from your system:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
skillup remove
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Update Skills
|
|
47
|
+
Update all installed skills to their latest tracked versions:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
skillup update
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or update a specific repository:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
skillup update --repo google/gemini-cli-skills
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. Sync Skills
|
|
60
|
+
Install all skills as defined in the lock file using the pinned commit SHAs (useful for setting up a new machine):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
skillup sync
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 5. Migrate from NPX Skills CLI
|
|
67
|
+
If you already have a `skills-lock.json` in your repository root, you can import it into the skillup lock format. The latest release or branch commit is resolved from GitHub at migration time.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
skillup migrate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A custom path can be provided if the file is elsewhere:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
skillup migrate path/to/skills-lock.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Skill Definition
|
|
80
|
+
A folder is recognized as a valid skill if it resides within a `skills/` directory at the repository root and contains a `SKILL.md` file.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Install dependencies
|
|
88
|
+
uv sync
|
|
89
|
+
|
|
90
|
+
# Run locally
|
|
91
|
+
uv run skillup --help
|
|
92
|
+
|
|
93
|
+
# Run tests
|
|
94
|
+
uv run pytest
|
|
95
|
+
|
|
96
|
+
# Type check
|
|
97
|
+
uv run pyright skillup
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skillup"
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "A minimal CLI to manage agent skills from GitHub releases."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer",
|
|
13
|
+
"rich",
|
|
14
|
+
"questionary",
|
|
15
|
+
"requests",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pyright",
|
|
21
|
+
"pytest",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
skillup = "skillup.cli:app"
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["skillup"]
|
|
File without changes
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
import questionary
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .github import get_repo_source
|
|
11
|
+
from .install import download_release, ensure_dirs, get_skills_in_zip, install_skill
|
|
12
|
+
from .lock import apply_source, get_sync_source, load_lock, normalize_repo_data, save_lock
|
|
13
|
+
from .settings import format_source_label, settings
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Minimal CLI to manage agent skills from GitHub releases or branches.")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def main(
|
|
21
|
+
is_global: bool = typer.Option(False, "--global", "-g", help="Use home directory instead of current directory"),
|
|
22
|
+
):
|
|
23
|
+
"""Minimal CLI to manage agent skills from GitHub releases or branches."""
|
|
24
|
+
settings.is_global = is_global
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def add(
|
|
29
|
+
repo: str = typer.Argument(..., help="GitHub repository (owner/repo)"),
|
|
30
|
+
skills: Optional[List[str]] = typer.Option(None, "--skill", "-s", help="Specific skill(s) to add (non-interactive)"),
|
|
31
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch to install from instead of the latest release"),
|
|
32
|
+
):
|
|
33
|
+
"""Add skills from a GitHub release or branch."""
|
|
34
|
+
ensure_dirs()
|
|
35
|
+
lock = load_lock()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
source = get_repo_source(repo, branch)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
console.print(f"[red]Error fetching source info for {repo}:[/red] {e}")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
zip_path = download_release(repo, source.cache_key, source.zip_url)
|
|
44
|
+
available_skills = get_skills_in_zip(zip_path)
|
|
45
|
+
|
|
46
|
+
repo_data = normalize_repo_data(lock["repos"].get(repo, {"skills": []}))
|
|
47
|
+
installed_skills = set(repo_data["skills"])
|
|
48
|
+
|
|
49
|
+
if skills:
|
|
50
|
+
selected = [s for s in skills if s in available_skills and s not in installed_skills]
|
|
51
|
+
invalid = [s for s in skills if s not in available_skills]
|
|
52
|
+
already_installed = [s for s in skills if s in installed_skills]
|
|
53
|
+
|
|
54
|
+
if invalid:
|
|
55
|
+
console.print(f"[yellow]Warning: Skills not found in {repo}: {', '.join(invalid)}[/yellow]")
|
|
56
|
+
if already_installed:
|
|
57
|
+
console.print(f"[blue]Note: Skills already installed from {repo}: {', '.join(already_installed)}[/blue]")
|
|
58
|
+
else:
|
|
59
|
+
to_show = [s for s in available_skills if s not in installed_skills]
|
|
60
|
+
|
|
61
|
+
if not to_show:
|
|
62
|
+
console.print(f"[yellow]No new skills available to add from {repo}.[/yellow]")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
selected = questionary.checkbox(
|
|
66
|
+
f"Select skills to add from {repo}:",
|
|
67
|
+
choices=to_show,
|
|
68
|
+
).ask()
|
|
69
|
+
|
|
70
|
+
if not selected:
|
|
71
|
+
console.print("No skills selected or available for installation.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
for skill in selected:
|
|
75
|
+
console.print(f"Installing [cyan]{skill}[/cyan]...")
|
|
76
|
+
install_skill(skill, zip_path)
|
|
77
|
+
if skill not in repo_data["skills"]:
|
|
78
|
+
repo_data["skills"].append(skill)
|
|
79
|
+
|
|
80
|
+
repo_data = apply_source(repo_data, source)
|
|
81
|
+
lock["repos"][repo] = repo_data
|
|
82
|
+
save_lock(lock)
|
|
83
|
+
console.print(f"[green]Skills from {repo} installed successfully![/green]")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def remove():
|
|
88
|
+
"""Interactively remove installed skills across all repositories."""
|
|
89
|
+
lock = load_lock()
|
|
90
|
+
|
|
91
|
+
all_installed = []
|
|
92
|
+
for repo, data in lock["repos"].items():
|
|
93
|
+
for skill in data["skills"]:
|
|
94
|
+
all_installed.append(f"{repo}: {skill}")
|
|
95
|
+
|
|
96
|
+
if not all_installed:
|
|
97
|
+
console.print("[yellow]No skills installed.[/yellow]")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
selected = questionary.checkbox(
|
|
101
|
+
"Select skills to remove:",
|
|
102
|
+
choices=all_installed,
|
|
103
|
+
).ask()
|
|
104
|
+
|
|
105
|
+
if not selected:
|
|
106
|
+
console.print("No skills selected.")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
for item in selected:
|
|
110
|
+
repo, skill = item.split(": ", 1)
|
|
111
|
+
console.print(f"Removing [red]{skill}[/red] from {repo}...")
|
|
112
|
+
for target_dir in [settings.skills_dir_agents, settings.skills_dir_claude]:
|
|
113
|
+
dest = target_dir / skill
|
|
114
|
+
if dest.exists():
|
|
115
|
+
import shutil
|
|
116
|
+
shutil.rmtree(dest)
|
|
117
|
+
|
|
118
|
+
lock["repos"][repo]["skills"].remove(skill)
|
|
119
|
+
if not lock["repos"][repo]["skills"]:
|
|
120
|
+
del lock["repos"][repo]
|
|
121
|
+
|
|
122
|
+
save_lock(lock)
|
|
123
|
+
console.print("[green]Skills removed successfully![/green]")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command()
|
|
127
|
+
def update(repo: Optional[str] = typer.Option(None, "--repo", help="Specific repository to update")):
|
|
128
|
+
"""Update installed skills to the latest tracked release or branch commit."""
|
|
129
|
+
lock = load_lock()
|
|
130
|
+
repos_to_update = [repo] if repo else list(lock["repos"].keys())
|
|
131
|
+
|
|
132
|
+
if not repos_to_update:
|
|
133
|
+
console.print("[yellow]No skills installed to update.[/yellow]")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
for r in repos_to_update:
|
|
137
|
+
if r not in lock["repos"]:
|
|
138
|
+
if repo:
|
|
139
|
+
console.print(f"[red]Repository {r} is not tracked.[/red]")
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
repo_data = normalize_repo_data(lock["repos"][r])
|
|
143
|
+
console.print(f"Checking updates for [cyan]{r}[/cyan]...")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
tracked_branch = repo_data.get("branch") if repo_data.get("source") == "branch" else None
|
|
147
|
+
source = get_repo_source(r, tracked_branch)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
console.print(f"[red]Error fetching source info for {r}:[/red] {e}")
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
current_version = repo_data.get("commit") or repo_data.get("tag") or repo_data.get("branch")
|
|
153
|
+
new_version = source.commit or source.ref
|
|
154
|
+
if current_version == new_version:
|
|
155
|
+
console.print(f" [green]{r} is already up-to-date ({format_source_label(source)}).[/green]")
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
previous_label = repo_data.get("tag") or repo_data.get("branch") or repo_data.get("commit", "unknown")
|
|
159
|
+
next_label = format_source_label(source)
|
|
160
|
+
console.print(f" Updating from {previous_label} to [cyan]{next_label}[/cyan]...", highlight=False)
|
|
161
|
+
zip_path = download_release(r, source.cache_key, source.zip_url)
|
|
162
|
+
|
|
163
|
+
for skill in repo_data["skills"]:
|
|
164
|
+
console.print(f" Updating [cyan]{skill}[/cyan]...")
|
|
165
|
+
install_skill(skill, zip_path)
|
|
166
|
+
|
|
167
|
+
repo_data = apply_source(repo_data, source)
|
|
168
|
+
lock["repos"][r] = repo_data
|
|
169
|
+
save_lock(lock)
|
|
170
|
+
console.print(f" [green]Successfully updated {r} to {next_label}![/green]")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def sync():
|
|
175
|
+
"""Install all skills as defined in the lock file."""
|
|
176
|
+
ensure_dirs()
|
|
177
|
+
lock = load_lock()
|
|
178
|
+
|
|
179
|
+
if not lock["repos"]:
|
|
180
|
+
console.print("[yellow]No skills defined in lock file. Use 'add' to add skills.[/yellow]")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
for repo, repo_data in lock["repos"].items():
|
|
184
|
+
source = get_sync_source(repo, repo_data)
|
|
185
|
+
skills = repo_data["skills"]
|
|
186
|
+
console.print(f"Syncing [cyan]{repo}[/cyan] at [green]{format_source_label(source)}[/green]...")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
zip_path = download_release(repo, source.cache_key, source.zip_url)
|
|
190
|
+
for skill in skills:
|
|
191
|
+
console.print(f" Installing [cyan]{skill}[/cyan]...")
|
|
192
|
+
install_skill(skill, zip_path)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
console.print(f"[red]Error syncing {repo}:[/red] {e}")
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
console.print("[green]Synchronization complete![/green]")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command()
|
|
201
|
+
def migrate(
|
|
202
|
+
input_file: Optional[Path] = typer.Argument(None, help="Path to skills-lock.json (defaults to .claude/skills-lock.json)"),
|
|
203
|
+
):
|
|
204
|
+
"""Migrate a Claude Code skills-lock.json to the skillup lock format."""
|
|
205
|
+
resolved = input_file if input_file is not None else settings.base_dir / "skills-lock.json"
|
|
206
|
+
|
|
207
|
+
if not resolved.exists():
|
|
208
|
+
console.print(f"[red]File not found: {resolved}[/red]")
|
|
209
|
+
raise typer.Exit(1)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
data = json.loads(resolved.read_text())
|
|
213
|
+
except Exception as e:
|
|
214
|
+
console.print(f"[red]Failed to parse {resolved}:[/red] {e}")
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
|
|
217
|
+
skills_map: dict[str, Any] = data.get("skills", {})
|
|
218
|
+
if not skills_map:
|
|
219
|
+
console.print("[yellow]No skills found in the input file.[/yellow]")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
repos: dict[str, list[str]] = {}
|
|
223
|
+
for skill_name, skill_data in skills_map.items():
|
|
224
|
+
if skill_data.get("sourceType") != "github":
|
|
225
|
+
console.print(f"[yellow]Skipping {skill_name}: unsupported sourceType '{skill_data.get('sourceType')}'[/yellow]")
|
|
226
|
+
continue
|
|
227
|
+
repo = skill_data["source"]
|
|
228
|
+
repos.setdefault(repo, []).append(skill_name)
|
|
229
|
+
|
|
230
|
+
if not repos:
|
|
231
|
+
console.print("[yellow]No GitHub skills found to migrate.[/yellow]")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
lock = load_lock()
|
|
235
|
+
|
|
236
|
+
for repo, skill_names in repos.items():
|
|
237
|
+
console.print(f"Resolving [cyan]{repo}[/cyan]...")
|
|
238
|
+
try:
|
|
239
|
+
source = get_repo_source(repo)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
console.print(f"[red]Error fetching source info for {repo}:[/red] {e}")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
repo_data = lock["repos"].get(repo, {"skills": []})
|
|
245
|
+
existing = set(repo_data.get("skills", []))
|
|
246
|
+
for skill in skill_names:
|
|
247
|
+
if skill not in existing:
|
|
248
|
+
repo_data.setdefault("skills", []).append(skill)
|
|
249
|
+
|
|
250
|
+
repo_data = apply_source(repo_data, source)
|
|
251
|
+
lock["repos"][repo] = repo_data
|
|
252
|
+
console.print(f" [green]Migrated {len(skill_names)} skill(s) at {format_source_label(source)}[/green]")
|
|
253
|
+
|
|
254
|
+
ensure_dirs()
|
|
255
|
+
save_lock(lock)
|
|
256
|
+
console.print(f"[green]Migration complete! Lock file saved to {settings.lock_file}[/green]")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
app()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .settings import RepoSource, settings
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_github_token() -> Optional[str]:
|
|
15
|
+
token = os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN")
|
|
16
|
+
if token:
|
|
17
|
+
return token
|
|
18
|
+
|
|
19
|
+
gh_path = shutil.which("gh")
|
|
20
|
+
if gh_path:
|
|
21
|
+
try:
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
[gh_path, "auth", "token"],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
check=True,
|
|
27
|
+
)
|
|
28
|
+
token = result.stdout.strip()
|
|
29
|
+
if token:
|
|
30
|
+
return token
|
|
31
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
console.print("[yellow]Warning: GitHub authentication not found. API rate limits may apply.[/yellow]")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_github_headers() -> dict[str, str]:
|
|
39
|
+
headers: dict[str, str] = {}
|
|
40
|
+
token = get_github_token()
|
|
41
|
+
if token:
|
|
42
|
+
headers["Authorization"] = f"token {token}"
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_latest_release(repo: str) -> tuple[str, str]:
|
|
47
|
+
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
|
48
|
+
response = requests.get(url, headers=get_github_headers())
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
data = response.json()
|
|
51
|
+
return data["tag_name"], data["zipball_url"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_commit_sha(repo: str, ref: str) -> str:
|
|
55
|
+
url = f"https://api.github.com/repos/{repo}/commits/{ref}"
|
|
56
|
+
response = requests.get(url, headers=get_github_headers())
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
data = response.json()
|
|
59
|
+
return data["sha"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_repo_source(repo: str, branch: Optional[str] = None) -> RepoSource:
|
|
63
|
+
if branch:
|
|
64
|
+
commit = get_commit_sha(repo, branch)
|
|
65
|
+
return RepoSource(
|
|
66
|
+
kind="branch",
|
|
67
|
+
ref=branch,
|
|
68
|
+
commit=commit,
|
|
69
|
+
zip_url=f"https://api.github.com/repos/{repo}/zipball/{commit}",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
tag, zip_url = get_latest_release(repo)
|
|
74
|
+
return RepoSource(
|
|
75
|
+
kind="release",
|
|
76
|
+
ref=tag,
|
|
77
|
+
commit=get_commit_sha(repo, tag),
|
|
78
|
+
zip_url=zip_url,
|
|
79
|
+
)
|
|
80
|
+
except requests.HTTPError as exc:
|
|
81
|
+
if exc.response is not None and exc.response.status_code == 404:
|
|
82
|
+
commit = get_commit_sha(repo, "main")
|
|
83
|
+
return RepoSource(
|
|
84
|
+
kind="branch",
|
|
85
|
+
ref="main",
|
|
86
|
+
commit=commit,
|
|
87
|
+
zip_url=f"https://api.github.com/repos/{repo}/zipball/{commit}",
|
|
88
|
+
)
|
|
89
|
+
raise
|