skillporter 2.0.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.
- skillporter-2.0.0/LICENSE +21 -0
- skillporter-2.0.0/PKG-INFO +258 -0
- skillporter-2.0.0/README.md +230 -0
- skillporter-2.0.0/pyproject.toml +68 -0
- skillporter-2.0.0/setup.cfg +4 -0
- skillporter-2.0.0/skillporter/__init__.py +16 -0
- skillporter-2.0.0/skillporter/__main__.py +4 -0
- skillporter-2.0.0/skillporter/atomic.py +55 -0
- skillporter-2.0.0/skillporter/cli.py +1288 -0
- skillporter-2.0.0/skillporter/diagnose.py +303 -0
- skillporter-2.0.0/skillporter/formats.py +168 -0
- skillporter-2.0.0/skillporter/frontmatter.py +242 -0
- skillporter-2.0.0/skillporter/harnesses.py +934 -0
- skillporter-2.0.0/skillporter/install.py +413 -0
- skillporter-2.0.0/skillporter/manifest.py +325 -0
- skillporter-2.0.0/skillporter/mcp_server.py +267 -0
- skillporter-2.0.0/skillporter/py.typed +0 -0
- skillporter-2.0.0/skillporter/registry.py +301 -0
- skillporter-2.0.0/skillporter/security.py +350 -0
- skillporter-2.0.0/skillporter/sources.py +361 -0
- skillporter-2.0.0/skillporter/state.py +261 -0
- skillporter-2.0.0/skillporter/ui.py +49 -0
- skillporter-2.0.0/skillporter.egg-info/PKG-INFO +258 -0
- skillporter-2.0.0/skillporter.egg-info/SOURCES.txt +31 -0
- skillporter-2.0.0/skillporter.egg-info/dependency_links.txt +1 -0
- skillporter-2.0.0/skillporter.egg-info/entry_points.txt +2 -0
- skillporter-2.0.0/skillporter.egg-info/requires.txt +4 -0
- skillporter-2.0.0/skillporter.egg-info/top_level.txt +1 -0
- skillporter-2.0.0/tests/test_archive_security.py +174 -0
- skillporter-2.0.0/tests/test_atomic_writes.py +144 -0
- skillporter-2.0.0/tests/test_regressions.py +65 -0
- skillporter-2.0.0/tests/test_skillporter.py +319 -0
- skillporter-2.0.0/tests/test_v2_ecosystem.py +619 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shanmukh Dheekonda
|
|
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.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skillporter
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: ONE SKILL. EVERY AGENT. Universal package manager, validator, security scanner, and synchronizer for AI-agent skills across 19+ harnesses.
|
|
5
|
+
Author: Shanmukh Dheekonda
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Shanmukh-l33t/skillporter
|
|
8
|
+
Project-URL: Issues, https://github.com/Shanmukh-l33t/skillporter/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Shanmukh-l33t/skillporter/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: agent-skills,claude-code,codex,cursor,windsurf,github-copilot,gemini-cli,hermes,openclaw,openhands,goose,mcp,developer-tools
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# SKILLPORTER
|
|
30
|
+
|
|
31
|
+
## ONE SKILL. EVERY AGENT.
|
|
32
|
+
|
|
33
|
+
**Install, manage, validate, security-scan, and synchronize AI-agent skills across the entire agent ecosystem.**
|
|
34
|
+
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](docs/harnesses/README.md)
|
|
37
|
+
[](tests/)
|
|
38
|
+
[](pyproject.toml)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="docs/assets/demo.svg" alt="Skillporter Terminal Demonstration" width="860" />
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
Every AI coding and autonomous agent harness reads skills from a **different directory**:
|
|
46
|
+
|
|
47
|
+
| Harness | Project Path | Personal / Global Path | Support |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| **Claude Code** | `.claude/skills/` | `~/.claude/skills/` | `NATIVE` |
|
|
50
|
+
| **OpenAI Codex** | `.agents/skills/` | `~/.agents/skills/` | `NATIVE` |
|
|
51
|
+
| **Cursor** | `.agents/skills/`, `.cursor/skills/` (+2 compat) | `~/.agents/skills/`, `~/.cursor/skills/` | `NATIVE` |
|
|
52
|
+
| **GitHub Copilot** | `.github/skills/`, `.agents/skills/` (+1 compat) | `~/.copilot/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
53
|
+
| **Gemini CLI** | `.agents/skills/`, `.gemini/skills/` | `~/.agents/skills/`, `~/.gemini/skills/` | `NATIVE` |
|
|
54
|
+
| **Google Antigravity** | `.agents/skills/` (+1 compat) | `~/.gemini/config/skills/` | `NATIVE` |
|
|
55
|
+
| **Windsurf** | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | `NATIVE` |
|
|
56
|
+
| **Cline** | `.cline/skills/`, `.clinerules/skills/` (+2 compat) | `~/.cline/skills/` | `NATIVE` |
|
|
57
|
+
| **Roo Code** | `.roo/skills/`, `.agents/skills/` | `~/.roo/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
58
|
+
| **OpenCode** | `.opencode/skills/`, `.agents/skills/` (+1 compat) | `~/.config/opencode/skills/` (+2 compat) | `NATIVE` |
|
|
59
|
+
| **Zed** | `.agents/skills/` | `~/.agents/skills/` | `NATIVE` |
|
|
60
|
+
| **Replit Agent** | `.agents/skills/`, `.claude/skills/` | `~/.config/agents/skills/` | `NATIVE` |
|
|
61
|
+
| **JetBrains Junie** | `.junie/skills/`, `.agents/skills/` | `~/.junie/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
62
|
+
| **Hermes Agent** | `.hermes/skills/` | `$HERMES_HOME/skills/<category>/<name>` | `NATIVE` |
|
|
63
|
+
| **OpenClaw** | `.openclaw/skills/`, `skills/`, `.agents/skills/` | `~/.openclaw/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
64
|
+
| **OpenHands** | `.agents/skills/`, `.openhands/skills/` | `~/.agents/skills/`, `~/.openhands/skills/` | `NATIVE` |
|
|
65
|
+
| **Goose** | `.agents/skills/`, `.goose/skills/` | `~/.agents/skills/`, `~/.config/goose/skills/` | `NATIVE` |
|
|
66
|
+
| **Continue** | `.continue/skills/` (`.continue/rules/` adapted) | `~/.continue/skills/` | `COMPATIBLE` |
|
|
67
|
+
| **Aider / AiderDesk** | `.aider-desk/skills/` (`CONVENTIONS.md` adapted) | `~/.aider-desk/skills/` | `ADAPTED` |
|
|
68
|
+
|
|
69
|
+
A skill authored for Claude Code in `.claude/skills/` is silently invisible to OpenAI Codex, Gemini CLI, Zed, and Windsurf. Hand-rolled symlinks break in Docker build contexts, `git worktree`, and Windows checkouts — while manual copies silently drift out of sync.
|
|
70
|
+
|
|
71
|
+
**Skillporter solves the entire lifecycle:** one command resolves skills from GitHub or local paths, runs a static security and supply-chain scan, deduplicates shared destinations (`.agents/skills`), tracks ownership and SHA-256 content hashes, detects content drift across copies, and supports deterministic lockfiles (`skillporter.lock`) and transactional rollback.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## One-Command Installation & Zero-Install Execution
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 1. Run immediately via npx directly from GitHub (zero install)
|
|
79
|
+
npx github:Shanmukh-l33t/skillporter diagnose .
|
|
80
|
+
|
|
81
|
+
# 2. Run immediately via uvx directly from GitHub (zero install)
|
|
82
|
+
uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter diagnose .
|
|
83
|
+
|
|
84
|
+
# 3. Install globally via curl (Linux/macOS — bypasses PEP 668 externally-managed-environment)
|
|
85
|
+
curl -fsSL https://raw.githubusercontent.com/Shanmukh-l33t/skillporter/main/install.sh | sh
|
|
86
|
+
|
|
87
|
+
# 4. Install globally via uv tool or pipx directly from GitHub
|
|
88
|
+
uv tool install git+https://github.com/Shanmukh-l33t/skillporter
|
|
89
|
+
pipx install git+https://github.com/Shanmukh-l33t/skillporter
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Native Agent Integration (MCP Server)
|
|
93
|
+
|
|
94
|
+
Register Skillporter directly inside Claude Code, Codex, Cursor, Windsurf, or Hermes so your agent can diagnose, scan, and synchronize skills on command:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add skillporter -- uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter mcp
|
|
98
|
+
codex mcp add skillporter -- uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter mcp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 60-Second Quick Start
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 1. Detect which AI agent harnesses are installed or active in your repo
|
|
107
|
+
skillporter detect .
|
|
108
|
+
|
|
109
|
+
# 2. Install a skill from GitHub across all detected harnesses (validates + security-scans first)
|
|
110
|
+
skillporter add anthropics/skills --skill pdf --harness all
|
|
111
|
+
|
|
112
|
+
# 3. Diagnose reachability, broken symlinks, duplicate loading, and SHA-256 content drift
|
|
113
|
+
skillporter diagnose .
|
|
114
|
+
|
|
115
|
+
# 4. Run a static supply-chain security scan on any skill directory
|
|
116
|
+
skillporter scan .
|
|
117
|
+
|
|
118
|
+
# 5. Initialize skillporter.yaml, synchronize teammates, and pin skillporter.lock
|
|
119
|
+
skillporter init
|
|
120
|
+
skillporter sync
|
|
121
|
+
skillporter lock
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Real-World Evidence: Why Skillporter Exists
|
|
127
|
+
|
|
128
|
+
Built after auditing the exact failure modes reported across upstream agent ecosystems:
|
|
129
|
+
|
|
130
|
+
- **[agentskills/agentskills#15](https://github.com/agentskills/agentskills/issues/15)** (130+ comments) — *"Claude is looking at `.claude/skills/` and GitHub Copilot is looking at `.github/skills/`… create symlinks that match the specific folder of each harness"*
|
|
131
|
+
- **[thn-inc/claude-tweaks#3000](https://github.com/thn-inc/claude-tweaks/issues/3000)** — skill resolution misses `.agents/skills` when the symlink is absent or inside a **git worktree**
|
|
132
|
+
- **[openclaw/openclaw#14593](https://github.com/openclaw/openclaw/issues/14593)** — symlinked skill installs fail inside Docker build contexts
|
|
133
|
+
- **[squne121/loop-protocol](https://github.com/squne121/loop-protocol/issues)** — replacing broken Codex skill symlinks with a fail-closed `.agents/skills` bridge
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Key Features
|
|
138
|
+
|
|
139
|
+
### 1. Universal Source Resolution (`skillporter add`)
|
|
140
|
+
Install skills from:
|
|
141
|
+
- **GitHub shorthand**: `skillporter add owner/repo` or `skillporter add owner/repo/skills/my-skill@v1.2.0`
|
|
142
|
+
- **GitHub tree URLs**: `skillporter add https://github.com/owner/repo/tree/main/skills/my-skill`
|
|
143
|
+
- **Git URLs & commit SHAs**: `skillporter add https://example.com/skills.git --ref ceb64c6`
|
|
144
|
+
- **ZIP & tar.gz archives**: `skillporter add ./bundle.zip` (hardened against Zip-Slip and symlink escapes)
|
|
145
|
+
- **Registry slugs**: `skillporter add registry:anthropics/skills`
|
|
146
|
+
- **Local directories**: `skillporter add ./authoring/my-skill --harness claude-code --harness codex`
|
|
147
|
+
|
|
148
|
+
### 2. SHA-256 Content-Drift & Duplicate Detection (`skillporter diagnose`)
|
|
149
|
+
Competing tools copy files once and report success forever. Skillporter hashes every file inside every installed skill copy (`SHA-256`).
|
|
150
|
+
- **Drift**: If you edit `.claude/skills/my-skill/SKILL.md` and forget to update `.agents/skills/my-skill/SKILL.md`, `skillporter diagnose` flags `DRIFT` with the exact diverging hashes and exits `1`.
|
|
151
|
+
- **Duplicate loading**: If a skill is installed in both `.agents/skills` and `.cursor/skills`, `diagnose` flags `DUPLICATE` so Cursor does not load the same skill twice.
|
|
152
|
+
|
|
153
|
+
### 3. Static Security & Supply-Chain Protection (`skillporter scan`)
|
|
154
|
+
Agent skills contain natural-language instructions and executable scripts. Before installing any skill, Skillporter inspects all Markdown and script files for:
|
|
155
|
+
- **Embedded secrets & private keys** (`sec-private-key`, `sec-aws-key`, `sec-github-token`, `sec-api-key`)
|
|
156
|
+
- **Prompt injection & covert overrides** (`sec-prompt-override`, `sec-hidden-unicode`, `sec-html-comment-instruction`)
|
|
157
|
+
- **Dangerous execution patterns** (`sec-curl-pipe-sh`, `sec-reverse-shell`, `sec-credential-harvest`, `sec-dynamic-eval`)
|
|
158
|
+
- **Filesystem escapes** (`sec-symlink-escape`, `sec-path-traversal`, `sec-binary-artifact`)
|
|
159
|
+
|
|
160
|
+
*Transparency note: Static scanning inspects files for known dangerous patterns and structural escapes; it does not guarantee runtime safety.*
|
|
161
|
+
|
|
162
|
+
### 4. Manifest (`skillporter.yaml`), Lockfile (`skillporter.lock`) & Rollback
|
|
163
|
+
Declare your project's skills in `skillporter.yaml`:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
schema_version: 1
|
|
167
|
+
project: acme-platform
|
|
168
|
+
scope: project
|
|
169
|
+
strategy: copy
|
|
170
|
+
security_policy: HIGH
|
|
171
|
+
harnesses:
|
|
172
|
+
- claude-code
|
|
173
|
+
- codex
|
|
174
|
+
- cursor
|
|
175
|
+
- github-copilot
|
|
176
|
+
skills:
|
|
177
|
+
- name: skillporter
|
|
178
|
+
source: Shanmukh-l33t/skillporter/skills/skillporter
|
|
179
|
+
ref: main
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Run `skillporter sync` to reconcile the workspace, `skillporter lock` to pin exact git commits and SHA-256 tree hashes in `skillporter.lock`, and `skillporter rollback <skill>` to restore the previous snapshot from `.skillporter/backups/` if an update introduces a regression.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## CLI Reference
|
|
187
|
+
|
|
188
|
+
| Command | Description |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `skillporter add <source>` | Resolve, validate, security-scan, and install skills from GitHub, Git, archive, or local path |
|
|
191
|
+
| `skillporter diagnose [path]` | Check reachability, broken symlinks, SHA-256 content drift, duplicates, and spec compliance |
|
|
192
|
+
| `skillporter doctor [path] [--fix]` | Run full workspace health check and optionally auto-repair missing/drifted installs |
|
|
193
|
+
| `skillporter scan [path]` | Run static supply-chain and prompt-injection security assessment |
|
|
194
|
+
| `skillporter validate [path]` | Validate `SKILL.md` frontmatter and body against the [Agent Skills spec](https://agentskills.io/specification) |
|
|
195
|
+
| `skillporter inspect <source>` | Inspect a local or remote skill package (files, scripts, security, adapted previews) before installing |
|
|
196
|
+
| `skillporter detect [path]` | Detect which AI agent harnesses are installed on the machine or active in the project |
|
|
197
|
+
| `skillporter matrix` | Print the 19-harness compatibility and verification matrix (`--json` supported) |
|
|
198
|
+
| `skillporter init [path]` | Generate `skillporter.yaml` from discovered skills and active harnesses |
|
|
199
|
+
| `skillporter sync [path]` | Reconcile workspace skills across target harnesses and update `skillporter.lock` |
|
|
200
|
+
| `skillporter lock [path]` | Write deterministic `skillporter.lock` with SHA-256 content tree hashes |
|
|
201
|
+
| `skillporter list [path]` | List managed and discovered skills with ownership, hashes, and active harnesses |
|
|
202
|
+
| `skillporter update [skill]` | Update tracked skills from their recorded upstream source |
|
|
203
|
+
| `skillporter rollback <skill>` | Restore a skill to its previous backup snapshot in `.skillporter/backups/` |
|
|
204
|
+
| `skillporter install <path>` | Low-level local directory installer (`--strategy copy\|symlink`, `--adapt`, `--force`) |
|
|
205
|
+
| `skillporter remove <skill>` | Remove a skill from target harnesses (`uninstall` alias supported) |
|
|
206
|
+
| `skillporter search [query]` | Search the seed and configured skill registry catalog |
|
|
207
|
+
| `skillporter info <target>` | Show detailed metadata for a harness adapter or registry package |
|
|
208
|
+
| `skillporter pack [path]` | Build an immutable `.tar.gz` release archive and `provenance.json` checksum manifest |
|
|
209
|
+
| `skillporter mcp` | Start the zero-dependency stdio JSON-RPC 2.0 MCP server |
|
|
210
|
+
|
|
211
|
+
### Exit Codes (CI Contract)
|
|
212
|
+
- `0` — Clean / all checks passed
|
|
213
|
+
- `1` — Findings detected (missing reachability, content drift, broken symlink, spec error, or security policy block)
|
|
214
|
+
- `2` — Usage or runtime error (invalid path, unknown harness, unsafe overwrite refusal)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## GitHub Action Integration
|
|
219
|
+
|
|
220
|
+
Validate `SKILL.md` compliance, scan for security risks, and verify harness reachability automatically on every pull request:
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
name: Skillporter Check
|
|
224
|
+
on: [push, pull_request]
|
|
225
|
+
jobs:
|
|
226
|
+
skills:
|
|
227
|
+
runs-on: ubuntu-latest
|
|
228
|
+
steps:
|
|
229
|
+
- uses: actions/checkout@v4
|
|
230
|
+
- uses: Shanmukh-l33t/skillporter@v2
|
|
231
|
+
with:
|
|
232
|
+
command: validate
|
|
233
|
+
path: .
|
|
234
|
+
run-security-scan: "true"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Documentation
|
|
240
|
+
|
|
241
|
+
- [Getting Started](docs/getting-started/README.md)
|
|
242
|
+
- [Installation & Distribution (uvx, npx, binaries, MCP)](docs/installation/README.md)
|
|
243
|
+
- [Supported Harnesses & Evidence Directory (19 Harnesses)](docs/harnesses/README.md)
|
|
244
|
+
- [CLI Command Reference](docs/commands/README.md)
|
|
245
|
+
- [Configuration (`skillporter.yaml` & `skillporter.lock`)](docs/configuration/README.md)
|
|
246
|
+
- [Security & Supply-Chain Model](docs/security/README.md)
|
|
247
|
+
- [System Architecture](ARCHITECTURE.md)
|
|
248
|
+
- [Adapter Development SDK](docs/adapters/README.md)
|
|
249
|
+
- [Registry & Immutable Packaging](docs/registry/README.md)
|
|
250
|
+
- [Troubleshooting](docs/troubleshooting/README.md)
|
|
251
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
252
|
+
- [Product Roadmap](ROADMAP.md)
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# SKILLPORTER
|
|
2
|
+
|
|
3
|
+
## ONE SKILL. EVERY AGENT.
|
|
4
|
+
|
|
5
|
+
**Install, manage, validate, security-scan, and synchronize AI-agent skills across the entire agent ecosystem.**
|
|
6
|
+
|
|
7
|
+
[](https://www.python.org/)
|
|
8
|
+
[](docs/harnesses/README.md)
|
|
9
|
+
[](tests/)
|
|
10
|
+
[](pyproject.toml)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="docs/assets/demo.svg" alt="Skillporter Terminal Demonstration" width="860" />
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Every AI coding and autonomous agent harness reads skills from a **different directory**:
|
|
18
|
+
|
|
19
|
+
| Harness | Project Path | Personal / Global Path | Support |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| **Claude Code** | `.claude/skills/` | `~/.claude/skills/` | `NATIVE` |
|
|
22
|
+
| **OpenAI Codex** | `.agents/skills/` | `~/.agents/skills/` | `NATIVE` |
|
|
23
|
+
| **Cursor** | `.agents/skills/`, `.cursor/skills/` (+2 compat) | `~/.agents/skills/`, `~/.cursor/skills/` | `NATIVE` |
|
|
24
|
+
| **GitHub Copilot** | `.github/skills/`, `.agents/skills/` (+1 compat) | `~/.copilot/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
25
|
+
| **Gemini CLI** | `.agents/skills/`, `.gemini/skills/` | `~/.agents/skills/`, `~/.gemini/skills/` | `NATIVE` |
|
|
26
|
+
| **Google Antigravity** | `.agents/skills/` (+1 compat) | `~/.gemini/config/skills/` | `NATIVE` |
|
|
27
|
+
| **Windsurf** | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | `NATIVE` |
|
|
28
|
+
| **Cline** | `.cline/skills/`, `.clinerules/skills/` (+2 compat) | `~/.cline/skills/` | `NATIVE` |
|
|
29
|
+
| **Roo Code** | `.roo/skills/`, `.agents/skills/` | `~/.roo/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
30
|
+
| **OpenCode** | `.opencode/skills/`, `.agents/skills/` (+1 compat) | `~/.config/opencode/skills/` (+2 compat) | `NATIVE` |
|
|
31
|
+
| **Zed** | `.agents/skills/` | `~/.agents/skills/` | `NATIVE` |
|
|
32
|
+
| **Replit Agent** | `.agents/skills/`, `.claude/skills/` | `~/.config/agents/skills/` | `NATIVE` |
|
|
33
|
+
| **JetBrains Junie** | `.junie/skills/`, `.agents/skills/` | `~/.junie/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
34
|
+
| **Hermes Agent** | `.hermes/skills/` | `$HERMES_HOME/skills/<category>/<name>` | `NATIVE` |
|
|
35
|
+
| **OpenClaw** | `.openclaw/skills/`, `skills/`, `.agents/skills/` | `~/.openclaw/skills/`, `~/.agents/skills/` | `NATIVE` |
|
|
36
|
+
| **OpenHands** | `.agents/skills/`, `.openhands/skills/` | `~/.agents/skills/`, `~/.openhands/skills/` | `NATIVE` |
|
|
37
|
+
| **Goose** | `.agents/skills/`, `.goose/skills/` | `~/.agents/skills/`, `~/.config/goose/skills/` | `NATIVE` |
|
|
38
|
+
| **Continue** | `.continue/skills/` (`.continue/rules/` adapted) | `~/.continue/skills/` | `COMPATIBLE` |
|
|
39
|
+
| **Aider / AiderDesk** | `.aider-desk/skills/` (`CONVENTIONS.md` adapted) | `~/.aider-desk/skills/` | `ADAPTED` |
|
|
40
|
+
|
|
41
|
+
A skill authored for Claude Code in `.claude/skills/` is silently invisible to OpenAI Codex, Gemini CLI, Zed, and Windsurf. Hand-rolled symlinks break in Docker build contexts, `git worktree`, and Windows checkouts — while manual copies silently drift out of sync.
|
|
42
|
+
|
|
43
|
+
**Skillporter solves the entire lifecycle:** one command resolves skills from GitHub or local paths, runs a static security and supply-chain scan, deduplicates shared destinations (`.agents/skills`), tracks ownership and SHA-256 content hashes, detects content drift across copies, and supports deterministic lockfiles (`skillporter.lock`) and transactional rollback.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## One-Command Installation & Zero-Install Execution
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Run immediately via npx directly from GitHub (zero install)
|
|
51
|
+
npx github:Shanmukh-l33t/skillporter diagnose .
|
|
52
|
+
|
|
53
|
+
# 2. Run immediately via uvx directly from GitHub (zero install)
|
|
54
|
+
uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter diagnose .
|
|
55
|
+
|
|
56
|
+
# 3. Install globally via curl (Linux/macOS — bypasses PEP 668 externally-managed-environment)
|
|
57
|
+
curl -fsSL https://raw.githubusercontent.com/Shanmukh-l33t/skillporter/main/install.sh | sh
|
|
58
|
+
|
|
59
|
+
# 4. Install globally via uv tool or pipx directly from GitHub
|
|
60
|
+
uv tool install git+https://github.com/Shanmukh-l33t/skillporter
|
|
61
|
+
pipx install git+https://github.com/Shanmukh-l33t/skillporter
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Native Agent Integration (MCP Server)
|
|
65
|
+
|
|
66
|
+
Register Skillporter directly inside Claude Code, Codex, Cursor, Windsurf, or Hermes so your agent can diagnose, scan, and synchronize skills on command:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add skillporter -- uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter mcp
|
|
70
|
+
codex mcp add skillporter -- uvx --from git+https://github.com/Shanmukh-l33t/skillporter skillporter mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 60-Second Quick Start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 1. Detect which AI agent harnesses are installed or active in your repo
|
|
79
|
+
skillporter detect .
|
|
80
|
+
|
|
81
|
+
# 2. Install a skill from GitHub across all detected harnesses (validates + security-scans first)
|
|
82
|
+
skillporter add anthropics/skills --skill pdf --harness all
|
|
83
|
+
|
|
84
|
+
# 3. Diagnose reachability, broken symlinks, duplicate loading, and SHA-256 content drift
|
|
85
|
+
skillporter diagnose .
|
|
86
|
+
|
|
87
|
+
# 4. Run a static supply-chain security scan on any skill directory
|
|
88
|
+
skillporter scan .
|
|
89
|
+
|
|
90
|
+
# 5. Initialize skillporter.yaml, synchronize teammates, and pin skillporter.lock
|
|
91
|
+
skillporter init
|
|
92
|
+
skillporter sync
|
|
93
|
+
skillporter lock
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Real-World Evidence: Why Skillporter Exists
|
|
99
|
+
|
|
100
|
+
Built after auditing the exact failure modes reported across upstream agent ecosystems:
|
|
101
|
+
|
|
102
|
+
- **[agentskills/agentskills#15](https://github.com/agentskills/agentskills/issues/15)** (130+ comments) — *"Claude is looking at `.claude/skills/` and GitHub Copilot is looking at `.github/skills/`… create symlinks that match the specific folder of each harness"*
|
|
103
|
+
- **[thn-inc/claude-tweaks#3000](https://github.com/thn-inc/claude-tweaks/issues/3000)** — skill resolution misses `.agents/skills` when the symlink is absent or inside a **git worktree**
|
|
104
|
+
- **[openclaw/openclaw#14593](https://github.com/openclaw/openclaw/issues/14593)** — symlinked skill installs fail inside Docker build contexts
|
|
105
|
+
- **[squne121/loop-protocol](https://github.com/squne121/loop-protocol/issues)** — replacing broken Codex skill symlinks with a fail-closed `.agents/skills` bridge
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Key Features
|
|
110
|
+
|
|
111
|
+
### 1. Universal Source Resolution (`skillporter add`)
|
|
112
|
+
Install skills from:
|
|
113
|
+
- **GitHub shorthand**: `skillporter add owner/repo` or `skillporter add owner/repo/skills/my-skill@v1.2.0`
|
|
114
|
+
- **GitHub tree URLs**: `skillporter add https://github.com/owner/repo/tree/main/skills/my-skill`
|
|
115
|
+
- **Git URLs & commit SHAs**: `skillporter add https://example.com/skills.git --ref ceb64c6`
|
|
116
|
+
- **ZIP & tar.gz archives**: `skillporter add ./bundle.zip` (hardened against Zip-Slip and symlink escapes)
|
|
117
|
+
- **Registry slugs**: `skillporter add registry:anthropics/skills`
|
|
118
|
+
- **Local directories**: `skillporter add ./authoring/my-skill --harness claude-code --harness codex`
|
|
119
|
+
|
|
120
|
+
### 2. SHA-256 Content-Drift & Duplicate Detection (`skillporter diagnose`)
|
|
121
|
+
Competing tools copy files once and report success forever. Skillporter hashes every file inside every installed skill copy (`SHA-256`).
|
|
122
|
+
- **Drift**: If you edit `.claude/skills/my-skill/SKILL.md` and forget to update `.agents/skills/my-skill/SKILL.md`, `skillporter diagnose` flags `DRIFT` with the exact diverging hashes and exits `1`.
|
|
123
|
+
- **Duplicate loading**: If a skill is installed in both `.agents/skills` and `.cursor/skills`, `diagnose` flags `DUPLICATE` so Cursor does not load the same skill twice.
|
|
124
|
+
|
|
125
|
+
### 3. Static Security & Supply-Chain Protection (`skillporter scan`)
|
|
126
|
+
Agent skills contain natural-language instructions and executable scripts. Before installing any skill, Skillporter inspects all Markdown and script files for:
|
|
127
|
+
- **Embedded secrets & private keys** (`sec-private-key`, `sec-aws-key`, `sec-github-token`, `sec-api-key`)
|
|
128
|
+
- **Prompt injection & covert overrides** (`sec-prompt-override`, `sec-hidden-unicode`, `sec-html-comment-instruction`)
|
|
129
|
+
- **Dangerous execution patterns** (`sec-curl-pipe-sh`, `sec-reverse-shell`, `sec-credential-harvest`, `sec-dynamic-eval`)
|
|
130
|
+
- **Filesystem escapes** (`sec-symlink-escape`, `sec-path-traversal`, `sec-binary-artifact`)
|
|
131
|
+
|
|
132
|
+
*Transparency note: Static scanning inspects files for known dangerous patterns and structural escapes; it does not guarantee runtime safety.*
|
|
133
|
+
|
|
134
|
+
### 4. Manifest (`skillporter.yaml`), Lockfile (`skillporter.lock`) & Rollback
|
|
135
|
+
Declare your project's skills in `skillporter.yaml`:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
schema_version: 1
|
|
139
|
+
project: acme-platform
|
|
140
|
+
scope: project
|
|
141
|
+
strategy: copy
|
|
142
|
+
security_policy: HIGH
|
|
143
|
+
harnesses:
|
|
144
|
+
- claude-code
|
|
145
|
+
- codex
|
|
146
|
+
- cursor
|
|
147
|
+
- github-copilot
|
|
148
|
+
skills:
|
|
149
|
+
- name: skillporter
|
|
150
|
+
source: Shanmukh-l33t/skillporter/skills/skillporter
|
|
151
|
+
ref: main
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Run `skillporter sync` to reconcile the workspace, `skillporter lock` to pin exact git commits and SHA-256 tree hashes in `skillporter.lock`, and `skillporter rollback <skill>` to restore the previous snapshot from `.skillporter/backups/` if an update introduces a regression.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## CLI Reference
|
|
159
|
+
|
|
160
|
+
| Command | Description |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `skillporter add <source>` | Resolve, validate, security-scan, and install skills from GitHub, Git, archive, or local path |
|
|
163
|
+
| `skillporter diagnose [path]` | Check reachability, broken symlinks, SHA-256 content drift, duplicates, and spec compliance |
|
|
164
|
+
| `skillporter doctor [path] [--fix]` | Run full workspace health check and optionally auto-repair missing/drifted installs |
|
|
165
|
+
| `skillporter scan [path]` | Run static supply-chain and prompt-injection security assessment |
|
|
166
|
+
| `skillporter validate [path]` | Validate `SKILL.md` frontmatter and body against the [Agent Skills spec](https://agentskills.io/specification) |
|
|
167
|
+
| `skillporter inspect <source>` | Inspect a local or remote skill package (files, scripts, security, adapted previews) before installing |
|
|
168
|
+
| `skillporter detect [path]` | Detect which AI agent harnesses are installed on the machine or active in the project |
|
|
169
|
+
| `skillporter matrix` | Print the 19-harness compatibility and verification matrix (`--json` supported) |
|
|
170
|
+
| `skillporter init [path]` | Generate `skillporter.yaml` from discovered skills and active harnesses |
|
|
171
|
+
| `skillporter sync [path]` | Reconcile workspace skills across target harnesses and update `skillporter.lock` |
|
|
172
|
+
| `skillporter lock [path]` | Write deterministic `skillporter.lock` with SHA-256 content tree hashes |
|
|
173
|
+
| `skillporter list [path]` | List managed and discovered skills with ownership, hashes, and active harnesses |
|
|
174
|
+
| `skillporter update [skill]` | Update tracked skills from their recorded upstream source |
|
|
175
|
+
| `skillporter rollback <skill>` | Restore a skill to its previous backup snapshot in `.skillporter/backups/` |
|
|
176
|
+
| `skillporter install <path>` | Low-level local directory installer (`--strategy copy\|symlink`, `--adapt`, `--force`) |
|
|
177
|
+
| `skillporter remove <skill>` | Remove a skill from target harnesses (`uninstall` alias supported) |
|
|
178
|
+
| `skillporter search [query]` | Search the seed and configured skill registry catalog |
|
|
179
|
+
| `skillporter info <target>` | Show detailed metadata for a harness adapter or registry package |
|
|
180
|
+
| `skillporter pack [path]` | Build an immutable `.tar.gz` release archive and `provenance.json` checksum manifest |
|
|
181
|
+
| `skillporter mcp` | Start the zero-dependency stdio JSON-RPC 2.0 MCP server |
|
|
182
|
+
|
|
183
|
+
### Exit Codes (CI Contract)
|
|
184
|
+
- `0` — Clean / all checks passed
|
|
185
|
+
- `1` — Findings detected (missing reachability, content drift, broken symlink, spec error, or security policy block)
|
|
186
|
+
- `2` — Usage or runtime error (invalid path, unknown harness, unsafe overwrite refusal)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## GitHub Action Integration
|
|
191
|
+
|
|
192
|
+
Validate `SKILL.md` compliance, scan for security risks, and verify harness reachability automatically on every pull request:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
name: Skillporter Check
|
|
196
|
+
on: [push, pull_request]
|
|
197
|
+
jobs:
|
|
198
|
+
skills:
|
|
199
|
+
runs-on: ubuntu-latest
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/checkout@v4
|
|
202
|
+
- uses: Shanmukh-l33t/skillporter@v2
|
|
203
|
+
with:
|
|
204
|
+
command: validate
|
|
205
|
+
path: .
|
|
206
|
+
run-security-scan: "true"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Documentation
|
|
212
|
+
|
|
213
|
+
- [Getting Started](docs/getting-started/README.md)
|
|
214
|
+
- [Installation & Distribution (uvx, npx, binaries, MCP)](docs/installation/README.md)
|
|
215
|
+
- [Supported Harnesses & Evidence Directory (19 Harnesses)](docs/harnesses/README.md)
|
|
216
|
+
- [CLI Command Reference](docs/commands/README.md)
|
|
217
|
+
- [Configuration (`skillporter.yaml` & `skillporter.lock`)](docs/configuration/README.md)
|
|
218
|
+
- [Security & Supply-Chain Model](docs/security/README.md)
|
|
219
|
+
- [System Architecture](ARCHITECTURE.md)
|
|
220
|
+
- [Adapter Development SDK](docs/adapters/README.md)
|
|
221
|
+
- [Registry & Immutable Packaging](docs/registry/README.md)
|
|
222
|
+
- [Troubleshooting](docs/troubleshooting/README.md)
|
|
223
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
224
|
+
- [Product Roadmap](ROADMAP.md)
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skillporter"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "ONE SKILL. EVERY AGENT. Universal package manager, validator, security scanner, and synchronizer for AI-agent skills across 19+ harnesses."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Shanmukh Dheekonda" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"agent-skills",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"cursor",
|
|
18
|
+
"windsurf",
|
|
19
|
+
"github-copilot",
|
|
20
|
+
"gemini-cli",
|
|
21
|
+
"hermes",
|
|
22
|
+
"openclaw",
|
|
23
|
+
"openhands",
|
|
24
|
+
"goose",
|
|
25
|
+
"mcp",
|
|
26
|
+
"developer-tools",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 5 - Production/Stable",
|
|
30
|
+
"Environment :: Console",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
36
|
+
"Topic :: Software Development :: Build Tools",
|
|
37
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
38
|
+
"Topic :: Security",
|
|
39
|
+
]
|
|
40
|
+
dependencies = []
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = ["pytest>=7", "ruff>=0.6"]
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
skillporter = "skillporter.cli:main"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/Shanmukh-l33t/skillporter"
|
|
50
|
+
Issues = "https://github.com/Shanmukh-l33t/skillporter/issues"
|
|
51
|
+
Changelog = "https://github.com/Shanmukh-l33t/skillporter/blob/main/CHANGELOG.md"
|
|
52
|
+
|
|
53
|
+
[tool.setuptools]
|
|
54
|
+
packages = ["skillporter"]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
skillporter = ["py.typed"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-q"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 120
|
|
65
|
+
target-version = "py39"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""skillporter — Universal package manager and diagnostics engine for AI-agent skills.
|
|
2
|
+
|
|
3
|
+
ONE SKILL. EVERY AGENT.
|
|
4
|
+
|
|
5
|
+
Install, validate, scan, synchronize, and manage Agent Skills across 19+ AI
|
|
6
|
+
coding and autonomous agent harnesses (Claude Code, OpenAI Codex, Cursor,
|
|
7
|
+
GitHub Copilot, Gemini CLI, Google Antigravity, Windsurf, Cline, Roo Code,
|
|
8
|
+
OpenCode, Zed, Replit Agent, JetBrains Junie, Hermes Agent, OpenClaw,
|
|
9
|
+
OpenHands, Goose, Continue, and Aider).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__version__ = "2.0.0"
|
|
15
|
+
|
|
16
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Crash-safe file writes.
|
|
2
|
+
|
|
3
|
+
A plain ``Path.write_text`` truncates the destination and then streams the new
|
|
4
|
+
content. If the process dies (OOM, Ctrl-C, power loss) between those two steps,
|
|
5
|
+
the user is left with a truncated or empty file — for skillporter that means a
|
|
6
|
+
corrupted state file, manifest or lockfile, which is precisely the
|
|
7
|
+
"failed operations must not leave the environment inconsistent" case.
|
|
8
|
+
|
|
9
|
+
``atomic_write_text`` writes to a sibling temp file in the *same directory*
|
|
10
|
+
(same filesystem, so ``os.replace`` is atomic) and renames it into place.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def atomic_write_text(path: Path, content: str, *, encoding: str = "utf-8") -> None:
|
|
22
|
+
"""Write *content* to *path* atomically.
|
|
23
|
+
|
|
24
|
+
The destination is either the previous content or the complete new content —
|
|
25
|
+
never a partial write. File permissions of an existing destination are
|
|
26
|
+
preserved so we do not silently widen them.
|
|
27
|
+
"""
|
|
28
|
+
path = Path(path)
|
|
29
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
existing_mode: int | None = None
|
|
32
|
+
try:
|
|
33
|
+
if path.exists():
|
|
34
|
+
existing_mode = path.stat().st_mode & 0o777
|
|
35
|
+
except OSError:
|
|
36
|
+
existing_mode = None
|
|
37
|
+
|
|
38
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
39
|
+
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
|
|
40
|
+
)
|
|
41
|
+
tmp = Path(tmp_name)
|
|
42
|
+
try:
|
|
43
|
+
with os.fdopen(fd, "w", encoding=encoding, newline="\n") as fh:
|
|
44
|
+
fh.write(content)
|
|
45
|
+
fh.flush()
|
|
46
|
+
os.fsync(fh.fileno())
|
|
47
|
+
if existing_mode is not None:
|
|
48
|
+
with contextlib.suppress(OSError):
|
|
49
|
+
os.chmod(tmp, existing_mode)
|
|
50
|
+
os.replace(tmp, path)
|
|
51
|
+
except BaseException:
|
|
52
|
+
# Never leave a temp file behind, even on KeyboardInterrupt.
|
|
53
|
+
with contextlib.suppress(OSError):
|
|
54
|
+
tmp.unlink()
|
|
55
|
+
raise
|