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.
Files changed (36) hide show
  1. loadout-0.3.0/.github/workflows/ci.yml +51 -0
  2. loadout-0.3.0/.github/workflows/release.yml +106 -0
  3. loadout-0.3.0/.gitignore +31 -0
  4. loadout-0.3.0/.pre-commit-config.yaml +17 -0
  5. loadout-0.3.0/.releaserc.json +46 -0
  6. loadout-0.3.0/LICENSE +21 -0
  7. loadout-0.3.0/PKG-INFO +364 -0
  8. loadout-0.3.0/README.md +331 -0
  9. loadout-0.3.0/pyproject.toml +66 -0
  10. loadout-0.3.0/src/loadout/__init__.py +66 -0
  11. loadout-0.3.0/src/loadout/_prompts.py +44 -0
  12. loadout-0.3.0/src/loadout/_transforms.py +77 -0
  13. loadout-0.3.0/src/loadout/_version.py +1 -0
  14. loadout-0.3.0/src/loadout/adapters/__init__.py +13 -0
  15. loadout-0.3.0/src/loadout/adapters/_base.py +144 -0
  16. loadout-0.3.0/src/loadout/adapters/_protocol.py +61 -0
  17. loadout-0.3.0/src/loadout/adapters/claude.py +44 -0
  18. loadout-0.3.0/src/loadout/adapters/cursor.py +60 -0
  19. loadout-0.3.0/src/loadout/adapters/opencode.py +36 -0
  20. loadout-0.3.0/src/loadout/callbacks.py +51 -0
  21. loadout-0.3.0/src/loadout/discovery.py +160 -0
  22. loadout-0.3.0/src/loadout/exceptions.py +41 -0
  23. loadout-0.3.0/src/loadout/installer.py +163 -0
  24. loadout-0.3.0/src/loadout/models.py +104 -0
  25. loadout-0.3.0/src/loadout/registry.py +61 -0
  26. loadout-0.3.0/tests/__init__.py +0 -0
  27. loadout-0.3.0/tests/adapters/__init__.py +0 -0
  28. loadout-0.3.0/tests/adapters/test_claude.py +106 -0
  29. loadout-0.3.0/tests/adapters/test_cursor.py +120 -0
  30. loadout-0.3.0/tests/adapters/test_opencode.py +87 -0
  31. loadout-0.3.0/tests/conftest.py +155 -0
  32. loadout-0.3.0/tests/test_discovery.py +109 -0
  33. loadout-0.3.0/tests/test_installer.py +91 -0
  34. loadout-0.3.0/tests/test_models.py +154 -0
  35. loadout-0.3.0/tests/test_registry.py +64 -0
  36. 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 }}
@@ -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.