loadout 0.3.0__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.
- loadout-0.3.0/.github/workflows/ci.yml +51 -0
- loadout-0.3.0/.github/workflows/release.yml +106 -0
- loadout-0.3.0/.gitignore +31 -0
- loadout-0.3.0/.pre-commit-config.yaml +17 -0
- loadout-0.3.0/.releaserc.json +46 -0
- loadout-0.3.0/LICENSE +21 -0
- loadout-0.3.0/PKG-INFO +364 -0
- loadout-0.3.0/README.md +331 -0
- loadout-0.3.0/pyproject.toml +66 -0
- loadout-0.3.0/src/loadout/__init__.py +66 -0
- loadout-0.3.0/src/loadout/_prompts.py +44 -0
- loadout-0.3.0/src/loadout/_transforms.py +77 -0
- loadout-0.3.0/src/loadout/_version.py +1 -0
- loadout-0.3.0/src/loadout/adapters/__init__.py +13 -0
- loadout-0.3.0/src/loadout/adapters/_base.py +144 -0
- loadout-0.3.0/src/loadout/adapters/_protocol.py +61 -0
- loadout-0.3.0/src/loadout/adapters/claude.py +44 -0
- loadout-0.3.0/src/loadout/adapters/cursor.py +60 -0
- loadout-0.3.0/src/loadout/adapters/opencode.py +36 -0
- loadout-0.3.0/src/loadout/callbacks.py +51 -0
- loadout-0.3.0/src/loadout/discovery.py +160 -0
- loadout-0.3.0/src/loadout/exceptions.py +41 -0
- loadout-0.3.0/src/loadout/installer.py +163 -0
- loadout-0.3.0/src/loadout/models.py +104 -0
- loadout-0.3.0/src/loadout/registry.py +61 -0
- loadout-0.3.0/tests/__init__.py +0 -0
- loadout-0.3.0/tests/adapters/__init__.py +0 -0
- loadout-0.3.0/tests/adapters/test_claude.py +106 -0
- loadout-0.3.0/tests/adapters/test_cursor.py +120 -0
- loadout-0.3.0/tests/adapters/test_opencode.py +87 -0
- loadout-0.3.0/tests/conftest.py +155 -0
- loadout-0.3.0/tests/test_discovery.py +109 -0
- loadout-0.3.0/tests/test_installer.py +91 -0
- loadout-0.3.0/tests/test_models.py +154 -0
- loadout-0.3.0/tests/test_registry.py +64 -0
- loadout-0.3.0/tests/test_transforms.py +84 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
pre-commit:
|
|
15
|
+
name: Pre-commit checks
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
run: uv python install 3.12
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --all-extras
|
|
28
|
+
|
|
29
|
+
- uses: pre-commit/action@v3.0.1
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Install uv
|
|
42
|
+
uses: astral-sh/setup-uv@v4
|
|
43
|
+
|
|
44
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
45
|
+
run: uv python install ${{ matrix.python-version }}
|
|
46
|
+
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync --all-extras
|
|
49
|
+
|
|
50
|
+
- name: Run tests
|
|
51
|
+
run: uv run pytest --cov=loadout --cov-report=term-missing
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
issues: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v4
|
|
24
|
+
|
|
25
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
+
run: uv python install ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: uv sync --all-extras
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: uv run ruff check src/
|
|
33
|
+
|
|
34
|
+
- name: Type check
|
|
35
|
+
run: uv run mypy src/
|
|
36
|
+
|
|
37
|
+
- name: Test
|
|
38
|
+
run: uv run pytest --cov=loadout --cov-report=term-missing
|
|
39
|
+
|
|
40
|
+
release:
|
|
41
|
+
needs: test
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
if: github.ref == 'refs/heads/main'
|
|
44
|
+
outputs:
|
|
45
|
+
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
|
|
46
|
+
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
with:
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
persist-credentials: false
|
|
52
|
+
|
|
53
|
+
- name: Setup Node.js
|
|
54
|
+
uses: actions/setup-node@v4
|
|
55
|
+
with:
|
|
56
|
+
node-version: 22
|
|
57
|
+
|
|
58
|
+
- name: Install semantic-release
|
|
59
|
+
run: >
|
|
60
|
+
npm install -g
|
|
61
|
+
semantic-release
|
|
62
|
+
@semantic-release/commit-analyzer
|
|
63
|
+
@semantic-release/release-notes-generator
|
|
64
|
+
@semantic-release/exec
|
|
65
|
+
@semantic-release/git
|
|
66
|
+
@semantic-release/github
|
|
67
|
+
conventional-changelog-conventionalcommits
|
|
68
|
+
|
|
69
|
+
- name: Run semantic-release
|
|
70
|
+
id: semantic
|
|
71
|
+
env:
|
|
72
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
73
|
+
run: |
|
|
74
|
+
OUTPUT=$(npx semantic-release 2>&1) || true
|
|
75
|
+
echo "$OUTPUT"
|
|
76
|
+
if echo "$OUTPUT" | grep -q "Published release"; then
|
|
77
|
+
VERSION=$(echo "$OUTPUT" | grep -oP 'Published release \K[0-9]+\.[0-9]+\.[0-9]+')
|
|
78
|
+
echo "new_release_published=true" >> "$GITHUB_OUTPUT"
|
|
79
|
+
echo "new_release_version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
80
|
+
else
|
|
81
|
+
echo "new_release_published=false" >> "$GITHUB_OUTPUT"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
publish:
|
|
85
|
+
needs: release
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
if: needs.release.outputs.new_release_published == 'true'
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
with:
|
|
91
|
+
ref: main
|
|
92
|
+
fetch-depth: 0
|
|
93
|
+
|
|
94
|
+
- name: Install uv
|
|
95
|
+
uses: astral-sh/setup-uv@v4
|
|
96
|
+
|
|
97
|
+
- name: Set up Python
|
|
98
|
+
run: uv python install 3.12
|
|
99
|
+
|
|
100
|
+
- name: Build package
|
|
101
|
+
run: uv build
|
|
102
|
+
|
|
103
|
+
- name: Publish to PyPI
|
|
104
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
105
|
+
with:
|
|
106
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
loadout-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
|
|
11
|
+
# Testing
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Type checking
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
|
|
19
|
+
# Ruff
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
|
|
22
|
+
# IDEs
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
|
|
27
|
+
# uv
|
|
28
|
+
uv.lock
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
- id: check-merge-conflict
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
13
|
+
rev: v0.11.4
|
|
14
|
+
hooks:
|
|
15
|
+
- id: ruff
|
|
16
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
17
|
+
- id: ruff-format
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
["@semantic-release/commit-analyzer", {
|
|
5
|
+
"preset": "conventionalcommits",
|
|
6
|
+
"releaseRules": [
|
|
7
|
+
{ "type": "feat", "release": "minor" },
|
|
8
|
+
{ "type": "fix", "release": "patch" },
|
|
9
|
+
{ "type": "perf", "release": "patch" },
|
|
10
|
+
{ "type": "refactor", "release": "patch" },
|
|
11
|
+
{ "type": "docs", "release": false },
|
|
12
|
+
{ "type": "style", "release": false },
|
|
13
|
+
{ "type": "test", "release": false },
|
|
14
|
+
{ "type": "chore", "release": false },
|
|
15
|
+
{ "type": "ci", "release": false },
|
|
16
|
+
{ "type": "build", "release": false },
|
|
17
|
+
{ "breaking": true, "release": "major" }
|
|
18
|
+
]
|
|
19
|
+
}],
|
|
20
|
+
["@semantic-release/release-notes-generator", {
|
|
21
|
+
"preset": "conventionalcommits",
|
|
22
|
+
"presetConfig": {
|
|
23
|
+
"types": [
|
|
24
|
+
{ "type": "feat", "section": "Features" },
|
|
25
|
+
{ "type": "fix", "section": "Bug Fixes" },
|
|
26
|
+
{ "type": "perf", "section": "Performance" },
|
|
27
|
+
{ "type": "refactor", "section": "Refactoring" },
|
|
28
|
+
{ "type": "docs", "section": "Documentation", "hidden": true },
|
|
29
|
+
{ "type": "chore", "hidden": true },
|
|
30
|
+
{ "type": "ci", "hidden": true },
|
|
31
|
+
{ "type": "build", "hidden": true },
|
|
32
|
+
{ "type": "test", "hidden": true },
|
|
33
|
+
{ "type": "style", "hidden": true }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}],
|
|
37
|
+
["@semantic-release/exec", {
|
|
38
|
+
"prepareCmd": "sed -i 's/__version__ = .*/__version__ = \"${nextRelease.version}\"/' src/loadout/_version.py"
|
|
39
|
+
}],
|
|
40
|
+
["@semantic-release/git", {
|
|
41
|
+
"assets": ["src/loadout/_version.py"],
|
|
42
|
+
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
|
|
43
|
+
}],
|
|
44
|
+
"@semantic-release/github"
|
|
45
|
+
]
|
|
46
|
+
}
|
loadout-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick MacCarthy
|
|
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.
|
loadout-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loadout
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Install artifacts (skills, rules, agents, commands) into coding agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/nickmaccarthy/loadout
|
|
6
|
+
Project-URL: Issues, https://github.com/nickmaccarthy/loadout/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/nickmaccarthy/loadout.git
|
|
8
|
+
Author: Nick MacCarthy
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: interactive
|
|
31
|
+
Requires-Dist: questionary>=2.0; extra == 'interactive'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# 🎒 loadout
|
|
35
|
+
|
|
36
|
+
**The package manager for AI coding agent artifacts.**
|
|
37
|
+
|
|
38
|
+
Stop manually copying skills, rules, agents, and commands into every coding agent's config directory. **loadout** handles discovery, transformation, and installation across Claude Code, Cursor, and OpenCode — so your CLI or project setup script doesn't have to.
|
|
39
|
+
|
|
40
|
+
## 📦 Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# uv (recommended)
|
|
44
|
+
uv add loadout
|
|
45
|
+
|
|
46
|
+
# pip
|
|
47
|
+
pip install loadout
|
|
48
|
+
|
|
49
|
+
# With interactive agent selection prompt
|
|
50
|
+
uv add "loadout[interactive]"
|
|
51
|
+
pip install "loadout[interactive]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🤔 Why loadout?
|
|
57
|
+
|
|
58
|
+
Every team building on top of AI coding agents hits the same problem: distributing custom skills, rules, and commands to developers' machines. The config directories differ, the file formats differ, and the logic for "install this skill to that agent" gets copy-pasted across tooling.
|
|
59
|
+
|
|
60
|
+
**loadout** extracts that logic into a single, typed, tested library with an extensible adapter pattern. You write your artifacts once. loadout puts them where they belong.
|
|
61
|
+
|
|
62
|
+
- 🌐 **Agent-agnostic** — one artifact definition works across all supported agents
|
|
63
|
+
- 🔍 **Auto-detection** — discovers which agents are installed on the system
|
|
64
|
+
- 📁 **Convention + configuration** — scan for marker files, or define a manifest
|
|
65
|
+
- 🔌 **Adapter pattern** — add support for new agents without touching core logic
|
|
66
|
+
- 🪝 **Lifecycle hooks** — plug in your own logging, progress bars, or analytics
|
|
67
|
+
- 🛡️ **Fully typed** — strict mypy, Pydantic models, protocol classes
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ⚡ Quick start
|
|
72
|
+
|
|
73
|
+
### 🚀 Install everything to every detected agent
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from loadout import install_all
|
|
77
|
+
|
|
78
|
+
summary = install_all("./my-artifacts", force=True)
|
|
79
|
+
|
|
80
|
+
print(f"✅ Installed: {len(summary.installed)}")
|
|
81
|
+
print(f"⏭️ Skipped: {len(summary.skipped)}")
|
|
82
|
+
print(f"❌ Failed: {len(summary.failed)}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 💬 Interactive mode (checkbox prompt)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv add "loadout[interactive]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from loadout import install_interactive
|
|
93
|
+
|
|
94
|
+
summary = install_interactive("./my-artifacts")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 🎛️ Full control
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from loadout import discover_artifacts, detect_agents, install
|
|
101
|
+
|
|
102
|
+
artifacts = discover_artifacts("./my-artifacts")
|
|
103
|
+
agents = detect_agents()
|
|
104
|
+
summary = install(artifacts, agents, force=True)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Three tiers — pick the one that fits your UX. 🎯
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🤖 Supported agents
|
|
112
|
+
|
|
113
|
+
| Agent | Config dir | Skills | Rules | Agents | Commands |
|
|
114
|
+
|---|---|---|---|---|---|
|
|
115
|
+
| **Claude Code** | `~/.claude/` | ✅ | ✅ | ✅ | ✅ |
|
|
116
|
+
| **Cursor** | `~/.cursor/` | ✅ | ✅ (.mdc) | — | — |
|
|
117
|
+
| **OpenCode** | `~/.opencode/` | ✅ | — | — | ✅ |
|
|
118
|
+
|
|
119
|
+
loadout auto-detects which agents are present and only installs to agents that support each artifact type. Unsupported combinations are cleanly skipped. ✨
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🔎 Artifact discovery
|
|
124
|
+
|
|
125
|
+
### 📂 Convention-based (marker files)
|
|
126
|
+
|
|
127
|
+
Drop marker files into your artifact directories and loadout will find them:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
my-artifacts/
|
|
131
|
+
login-skill/
|
|
132
|
+
SKILL.md # ← marker: this directory is a skill
|
|
133
|
+
helper.py
|
|
134
|
+
utils.py
|
|
135
|
+
security/
|
|
136
|
+
auth-rule/
|
|
137
|
+
RULE.md # ← marker: this file is a rule
|
|
138
|
+
setup-agent/
|
|
139
|
+
AGENT.md # ← marker: this file is an agent
|
|
140
|
+
deploy/
|
|
141
|
+
COMMAND.md # ← marker: this file is a command
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Marker files double as the artifact content — the `SKILL.md` **is** the skill. Categories are derived from directory structure (`security/auth-rule/` → category `security`).
|
|
145
|
+
|
|
146
|
+
### 📋 Manifest-based (loadout.yaml)
|
|
147
|
+
|
|
148
|
+
For explicit control, add a `loadout.yaml` to the root of your artifacts directory:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
artifacts:
|
|
152
|
+
- name: login-skill
|
|
153
|
+
type: skill
|
|
154
|
+
path: login-skill
|
|
155
|
+
|
|
156
|
+
- name: auth-rule
|
|
157
|
+
type: rule
|
|
158
|
+
path: security/auth-rule/RULE.md
|
|
159
|
+
category: security
|
|
160
|
+
description: "Enforces authentication checks"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When a manifest is present, marker-file scanning is skipped entirely — you have full control over what gets installed.
|
|
164
|
+
|
|
165
|
+
### 📝 Frontmatter support
|
|
166
|
+
|
|
167
|
+
Artifact files can include YAML frontmatter for metadata:
|
|
168
|
+
|
|
169
|
+
```markdown
|
|
170
|
+
---
|
|
171
|
+
description: Handles user login flows
|
|
172
|
+
globs:
|
|
173
|
+
- "src/auth/**"
|
|
174
|
+
always_apply: true
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
# Login Skill
|
|
178
|
+
|
|
179
|
+
Your skill content here...
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🔌 Custom adapters
|
|
185
|
+
|
|
186
|
+
Need to support a new coding agent? Implement the `AgentAdapter` interface and register it:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from pathlib import Path
|
|
190
|
+
from loadout import (
|
|
191
|
+
AgentAdapter,
|
|
192
|
+
Artifact,
|
|
193
|
+
ArtifactType,
|
|
194
|
+
DetectedAgent,
|
|
195
|
+
InstallResult,
|
|
196
|
+
get_default_registry,
|
|
197
|
+
install_all,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
class WindsurfAdapter(AgentAdapter):
|
|
201
|
+
@property
|
|
202
|
+
def agent_name(self) -> str:
|
|
203
|
+
return "windsurf"
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def display_name(self) -> str:
|
|
207
|
+
return "Windsurf"
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def config_dir_name(self) -> str:
|
|
211
|
+
return ".windsurf"
|
|
212
|
+
|
|
213
|
+
def supported_artifact_types(self) -> set[ArtifactType]:
|
|
214
|
+
return {ArtifactType.SKILL, ArtifactType.RULE}
|
|
215
|
+
|
|
216
|
+
def detect(self) -> DetectedAgent | None:
|
|
217
|
+
config_dir = Path.home() / self.config_dir_name
|
|
218
|
+
if config_dir.is_dir():
|
|
219
|
+
return DetectedAgent(
|
|
220
|
+
name=self.agent_name,
|
|
221
|
+
config_dir=config_dir,
|
|
222
|
+
display_name=self.display_name,
|
|
223
|
+
)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def get_target_path(self, artifact: Artifact, config_dir: Path) -> Path:
|
|
227
|
+
# Your path resolution logic
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
def transform_content(self, artifact: Artifact, content: str) -> str:
|
|
231
|
+
# Your content transformation logic
|
|
232
|
+
return content
|
|
233
|
+
|
|
234
|
+
def transform_filename(self, artifact: Artifact, filename: str) -> str:
|
|
235
|
+
return filename
|
|
236
|
+
|
|
237
|
+
def install(self, artifact: Artifact, agent: DetectedAgent, force: bool = False) -> InstallResult:
|
|
238
|
+
# Your install logic
|
|
239
|
+
...
|
|
240
|
+
|
|
241
|
+
# Register and use 🎉
|
|
242
|
+
registry = get_default_registry()
|
|
243
|
+
registry.register(WindsurfAdapter())
|
|
244
|
+
|
|
245
|
+
summary = install_all("./my-artifacts", registry=registry)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The adapter pattern means core loadout never needs to change when new agents appear. 🧩
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 🪝 Lifecycle callbacks
|
|
253
|
+
|
|
254
|
+
Hook into every stage of the installation process for logging, progress bars, analytics, or custom error handling:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from loadout import LoadoutCallbacks, Artifact, DetectedAgent, InstallResult, install_all
|
|
258
|
+
|
|
259
|
+
class RichCallbacks:
|
|
260
|
+
"""Example: pretty-print progress with Rich."""
|
|
261
|
+
|
|
262
|
+
def on_artifact_discovered(self, artifact: Artifact) -> None:
|
|
263
|
+
print(f" 🔎 Found {artifact.artifact_type.value}: {artifact.name}")
|
|
264
|
+
|
|
265
|
+
def on_agent_detected(self, agent: DetectedAgent) -> None:
|
|
266
|
+
print(f" 🤖 Detected agent: {agent.display_name}")
|
|
267
|
+
|
|
268
|
+
def on_install_started(self, artifact: Artifact, agent: DetectedAgent) -> None:
|
|
269
|
+
print(f" ⏳ Installing {artifact.name} → {agent.display_name}...")
|
|
270
|
+
|
|
271
|
+
def on_install_complete(self, result: InstallResult) -> None:
|
|
272
|
+
print(f" ✅ Installed to {result.target_path}")
|
|
273
|
+
|
|
274
|
+
def on_install_skipped(self, result: InstallResult) -> None:
|
|
275
|
+
print(f" ⏭️ Skipped: {result.error}")
|
|
276
|
+
|
|
277
|
+
def on_install_failed(self, result: InstallResult) -> None:
|
|
278
|
+
print(f" 💥 FAILED: {result.error}")
|
|
279
|
+
|
|
280
|
+
summary = install_all("./my-artifacts", callbacks=RichCallbacks())
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Only override the hooks you care about — the `LoadoutCallbacks` protocol defines the full interface, and `NoOpCallbacks` provides a ready-made base with no-op defaults.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 📖 API reference
|
|
288
|
+
|
|
289
|
+
### ⚙️ Top-level functions
|
|
290
|
+
|
|
291
|
+
| Function | Description |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `install_all(source_dir, force, registry, callbacks)` | Discover artifacts, detect agents, install everything |
|
|
294
|
+
| `install_interactive(source_dir, force, registry, callbacks)` | Same as above with interactive agent selection |
|
|
295
|
+
| `install(artifacts, agents, force, registry, callbacks)` | Install specific artifacts to specific agents |
|
|
296
|
+
| `discover_artifacts(source_dir)` | Scan a directory and return a list of `Artifact` objects |
|
|
297
|
+
| `detect_agents(registry)` | Detect installed coding agents |
|
|
298
|
+
| `get_default_registry()` | Get the built-in adapter registry |
|
|
299
|
+
|
|
300
|
+
### 🧱 Models
|
|
301
|
+
|
|
302
|
+
| Model | Description |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `Artifact` | A discovered artifact (name, type, source path, category, frontmatter) |
|
|
305
|
+
| `ArtifactType` | Enum: `SKILL`, `RULE`, `AGENT`, `COMMAND` |
|
|
306
|
+
| `DetectedAgent` | An agent found on the system (name, config dir, display name) |
|
|
307
|
+
| `InstallResult` | Result of a single artifact install (status, target path, error) |
|
|
308
|
+
| `InstallSummary` | Batch result with `.installed`, `.skipped`, `.failed`, `.already_existed` |
|
|
309
|
+
| `Manifest` | Parsed `loadout.yaml` manifest |
|
|
310
|
+
|
|
311
|
+
### 🚨 Exceptions
|
|
312
|
+
|
|
313
|
+
| Exception | Description |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `LoadoutError` | Base exception for all loadout errors |
|
|
316
|
+
| `ArtifactNotFoundError` | Source artifact path does not exist |
|
|
317
|
+
| `ManifestError` | Invalid `loadout.yaml` |
|
|
318
|
+
| `InstallError` | Installation failed |
|
|
319
|
+
| `AdapterNotFoundError` | No adapter registered for the given agent |
|
|
320
|
+
| `AdapterAlreadyRegisteredError` | Adapter name collision |
|
|
321
|
+
| `TransformError` | Content transformation failed |
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 🛠️ Development
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Clone and install with all extras
|
|
329
|
+
git clone https://github.com/nickmaccarthy/loadout.git
|
|
330
|
+
cd loadout
|
|
331
|
+
|
|
332
|
+
# uv (recommended)
|
|
333
|
+
uv sync --all-extras
|
|
334
|
+
|
|
335
|
+
# pip
|
|
336
|
+
pip install -e ".[dev,interactive]"
|
|
337
|
+
|
|
338
|
+
# Set up pre-commit hooks
|
|
339
|
+
uv run pre-commit install
|
|
340
|
+
|
|
341
|
+
# Run all pre-commit checks (ruff, mypy, formatting, etc.)
|
|
342
|
+
uv run pre-commit run --all-files
|
|
343
|
+
|
|
344
|
+
# Run tests
|
|
345
|
+
uv run pytest
|
|
346
|
+
|
|
347
|
+
# Run tests with coverage
|
|
348
|
+
uv run pytest --cov=loadout --cov-report=term-missing
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
> 💡 **Pre-commit hooks** run automatically on every `git commit`, catching lint errors, type issues, and formatting problems before they hit CI.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 📋 Requirements
|
|
356
|
+
|
|
357
|
+
- 🐍 Python 3.10+
|
|
358
|
+
- [pydantic](https://docs.pydantic.dev/) >= 2.0
|
|
359
|
+
- [PyYAML](https://pyyaml.org/) >= 6.0
|
|
360
|
+
- [questionary](https://questionary.readthedocs.io/) >= 2.0 *(optional, for interactive mode)*
|
|
361
|
+
|
|
362
|
+
## 📄 License
|
|
363
|
+
|
|
364
|
+
MIT — see [LICENSE](LICENSE) for details.
|