skman 0.1.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.
- skman-0.1.0/.gitignore +8 -0
- skman-0.1.0/LICENSE +21 -0
- skman-0.1.0/PKG-INFO +372 -0
- skman-0.1.0/README.md +325 -0
- skman-0.1.0/install.sh +95 -0
- skman-0.1.0/pyproject.toml +64 -0
- skman-0.1.0/skman/__init__.py +1 -0
- skman-0.1.0/skman/__main__.py +4 -0
- skman-0.1.0/skman/cli.py +258 -0
- skman-0.1.0/skman/links.py +83 -0
- skman-0.1.0/skman/migrate.py +418 -0
- skman-0.1.0/skman/sources.py +231 -0
- skman-0.1.0/skman/stats.py +108 -0
- skman-0.1.0/skman/sync.py +175 -0
- skman-0.1.0/skman/util.py +196 -0
- skman-0.1.0/tests/test_basic.py +608 -0
- skman-0.1.0/tests/test_migrate.py +553 -0
skman-0.1.0/.gitignore
ADDED
skman-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zhendong Liu
|
|
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.
|
skman-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Manage and sync coding-agent skills (Claude Code, Codex, skills.sh)
|
|
5
|
+
Project-URL: Homepage, https://github.com/zhendong/skill-man
|
|
6
|
+
Project-URL: Repository, https://github.com/zhendong/skill-man
|
|
7
|
+
Project-URL: Issues, https://github.com/zhendong/skill-man/issues
|
|
8
|
+
Author-email: Zhendong Liu <lzd110@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Zhendong Liu
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: agent-skills,ai-agents,claude-code,cli,codex,developer-tools,skills.sh
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: MacOS
|
|
36
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Software Development
|
|
44
|
+
Classifier: Topic :: Utilities
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# skman
|
|
49
|
+
|
|
50
|
+
A dead simple CLI for managing skills used by coding agents — works with **Claude Code**,
|
|
51
|
+
**Codex CLI**, and any other agent that discovers skills from `~/.agents/skills`
|
|
52
|
+
(the cross-agent dir; also where `skills.sh` / `npx skills` installs).
|
|
53
|
+
|
|
54
|
+
## What it does
|
|
55
|
+
|
|
56
|
+
1. **Download** skills from git repos (or local directories).
|
|
57
|
+
2. **Sync** them on demand — pulls upstream, refreshes state, updates symlinks.
|
|
58
|
+
3. **Symlink** every managed skill into `~/.agents/skills` and
|
|
59
|
+
`~/.claude/skills`. Codex picks the same skills up automatically via its
|
|
60
|
+
cross-agent fallback to `~/.agents/skills`. The dirs are created on first
|
|
61
|
+
sync — nothing to set up beforehand.
|
|
62
|
+
4. **Track state** in `~/.skman/state.json`: slug, name, description,
|
|
63
|
+
source, short commit id, install/last-sync times, and enabled flag
|
|
64
|
+
per skill.
|
|
65
|
+
5. **Disambiguate** skills from different sources by suffixing each
|
|
66
|
+
symlink with a short id derived from the source URL, so two sources
|
|
67
|
+
shipping the same skill name coexist without collision. A warning is
|
|
68
|
+
still printed when `(name, description)` matches across sources, so
|
|
69
|
+
you can spot true duplicates.
|
|
70
|
+
6. **Record usage** via a Claude Code `PreToolUse` hook and show aggregate
|
|
71
|
+
stats.
|
|
72
|
+
|
|
73
|
+
> Note: pluggable user-edits-as-patches is intentionally out of scope for now.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
### One-line install (recommended)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
curl -fsSL https://raw.githubusercontent.com/zhendong/skill-man/main/install.sh | sh
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Works on macOS and Linux. The installer uses [uv](https://docs.astral.sh/uv/)
|
|
84
|
+
to fetch a Python toolchain and install `skman` from PyPI into an isolated
|
|
85
|
+
environment — you don't need Python or pip beforehand.
|
|
86
|
+
|
|
87
|
+
Env overrides:
|
|
88
|
+
- `SKMAN_FROM_GIT=1` — install from the GitHub repo instead of PyPI (and
|
|
89
|
+
`SKMAN_REF=<branch-or-tag>` to pick a ref).
|
|
90
|
+
- `SKMAN_NO_UV=1` — fall back to `pipx`/`pip` instead of uv.
|
|
91
|
+
|
|
92
|
+
### Via pip / pipx / uv
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pipx install skman # recommended for global CLI install
|
|
96
|
+
uv tool install skman # uv equivalent
|
|
97
|
+
pip install --user skman # plain pip
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### From source
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd skill-man # repo dir keeps its name; the tool is `skman`
|
|
104
|
+
pip install -e . # exposes `skman` on PATH
|
|
105
|
+
# or:
|
|
106
|
+
uv tool install .
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can also run it without installing:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python3 -m skman <args>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
State lives in `~/.skman/` (override with `$SKMAN_ROOT`).
|
|
116
|
+
|
|
117
|
+
### Windows
|
|
118
|
+
|
|
119
|
+
There is no native Windows build. Use **WSL** (Windows Subsystem for Linux) —
|
|
120
|
+
install a distro (Ubuntu/Debian/etc.), open its shell, and run the one-line
|
|
121
|
+
install above from inside the Linux environment. Your agent CLI (Claude
|
|
122
|
+
Code, Codex, etc.) should also run inside WSL so skman's symlinks land in
|
|
123
|
+
the Linux home dir where the agent looks for them.
|
|
124
|
+
|
|
125
|
+
## First-run setup
|
|
126
|
+
|
|
127
|
+
After install, the fastest way to a working state is:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
skman setup
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This installs the Claude Code usage hook and migrates any skills already
|
|
134
|
+
on disk (see below). It's safe to re-run.
|
|
135
|
+
|
|
136
|
+
## Quick start
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
skman source add https://github.com/obra/superpowers.git # slug auto-derived as `superpowers`
|
|
140
|
+
skman sync # clones, finds SKILL.md files, links into both target dirs
|
|
141
|
+
|
|
142
|
+
skman list # see what's managed (with install/update times + commit)
|
|
143
|
+
skman install-hook --write # records skill invocations
|
|
144
|
+
skman stats # see what got used
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
There is no `init` step. All directories — including `~/.agents/skills` and
|
|
148
|
+
`~/.claude/skills` — are created the first time something needs to write into
|
|
149
|
+
them.
|
|
150
|
+
|
|
151
|
+
### Source layout convention
|
|
152
|
+
|
|
153
|
+
Sources follow the standard pattern: a top-level `skills/` directory holding
|
|
154
|
+
one folder per skill, each with a `SKILL.md` plus any helper files:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
<source-repo>/
|
|
158
|
+
└── skills/
|
|
159
|
+
├── brainstorming/
|
|
160
|
+
│ └── SKILL.md
|
|
161
|
+
└── tdd/
|
|
162
|
+
├── SKILL.md
|
|
163
|
+
└── examples/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
skman auto-detects: if `skills/` exists at the source root it scans
|
|
167
|
+
there; otherwise it scans the whole repo. Sub-categorisation (e.g.
|
|
168
|
+
`skills/foundations/tdd/`) is fine — `SKILL.md` is found recursively.
|
|
169
|
+
|
|
170
|
+
### Source identifiers
|
|
171
|
+
|
|
172
|
+
You don't pick a name. The slug is derived from the URL's last path segment
|
|
173
|
+
(lowercased, `.git` stripped, unsafe chars replaced):
|
|
174
|
+
|
|
175
|
+
| Input URL | Derived slug |
|
|
176
|
+
|----------------------------------------------------|----------------------|
|
|
177
|
+
| `https://github.com/obra/superpowers.git` | `superpowers` |
|
|
178
|
+
| `git@github.com:obra/superpowers` | `superpowers` |
|
|
179
|
+
| `/Users/me/dev/my-skills` | `my-skills` |
|
|
180
|
+
| second repo whose last segment is also `superpowers` | `superpowers-2` |
|
|
181
|
+
|
|
182
|
+
Adding the same URL twice errors out — `https://h/o/r`, `https://h/o/r/`,
|
|
183
|
+
`https://h/o/r.git`, and `git@h:o/r` are all recognised as the same source.
|
|
184
|
+
Remove with `skman source remove <slug>` or `skman source remove <url>`.
|
|
185
|
+
|
|
186
|
+
## State
|
|
187
|
+
|
|
188
|
+
Everything lives in one JSON file: `~/.skman/state.json`.
|
|
189
|
+
|
|
190
|
+
```jsonc
|
|
191
|
+
{
|
|
192
|
+
"version": 1,
|
|
193
|
+
"sources": {
|
|
194
|
+
"superpowers": { "type": "git", "url": "...", "ref": "main" }
|
|
195
|
+
},
|
|
196
|
+
"skills": {
|
|
197
|
+
"brainstorming-ab12cd": {
|
|
198
|
+
"slug": "brainstorming",
|
|
199
|
+
"name": "brainstorming",
|
|
200
|
+
"description": "You MUST use this before any creative work...",
|
|
201
|
+
"source": "superpowers",
|
|
202
|
+
"path": "skills/brainstorming",
|
|
203
|
+
"commit": "a1b2c3d",
|
|
204
|
+
"installed_at": "2026-05-14T10:00:00+00:00",
|
|
205
|
+
"updated_at": "2026-05-14T12:00:00+00:00",
|
|
206
|
+
"enabled": true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The map key (`brainstorming-ab12cd`) is also the symlink name in the
|
|
213
|
+
target dirs. The `-ab12cd` suffix is a 6-char hash of the source URL —
|
|
214
|
+
it lets two sources share a slug without collision.
|
|
215
|
+
|
|
216
|
+
`skman list` renders the state as a table:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
LINK NAME SLUG SOURCE COMMIT STATUS INSTALLED UPDATED
|
|
220
|
+
brainstorming-ab12cd brainstorming superpowers a1b2c3d enabled 2026-05-14 10:00 2026-05-14 12:00
|
|
221
|
+
tdd-ab12cd tdd superpowers a1b2c3d enabled 2026-05-14 10:00 2026-05-14 12:00
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Duplicate detection
|
|
225
|
+
|
|
226
|
+
After every sync, skman groups skills by `(name, description)` from their
|
|
227
|
+
SKILL.md frontmatter and prints a warning when any pair appears in more
|
|
228
|
+
than one state entry — e.g. when two sources both ship a `brainstorming`
|
|
229
|
+
skill with identical frontmatter.
|
|
230
|
+
|
|
231
|
+
The warning is informational: both skills remain installed. Symlink names
|
|
232
|
+
include a short id derived from the source URL (`brainstorming-ab12cd`,
|
|
233
|
+
`brainstorming-ef34gh`), so there's no collision at the filesystem level.
|
|
234
|
+
Resolve true duplicates by removing one of the sources, or by disabling
|
|
235
|
+
one with `skman disable <link-name>`.
|
|
236
|
+
|
|
237
|
+
## Stats
|
|
238
|
+
|
|
239
|
+
`skman install-hook --write` adds a Claude Code `PreToolUse` hook so
|
|
240
|
+
every Skill tool call is recorded to `~/.skman/stats/usage.jsonl`.
|
|
241
|
+
`skman stats` aggregates:
|
|
242
|
+
|
|
243
|
+
- per-skill invocation count, distinct sessions, last-used time
|
|
244
|
+
- count of managed skills that went unused in the window
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
skman stats # last 30 days
|
|
248
|
+
skman stats --days 7
|
|
249
|
+
skman stats --skill brainstorming
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Migrating from other tools
|
|
253
|
+
|
|
254
|
+
If you've been using Claude Code, Codex, or `skills.sh` (`npx skills …`),
|
|
255
|
+
you'll likely have skills scattered across these dirs:
|
|
256
|
+
|
|
257
|
+
- `~/.claude/skills/*` — Claude Code personal skills
|
|
258
|
+
- `~/.codex/skills/*` — Codex personal skills (`.system/` is skipped — Codex
|
|
259
|
+
built-ins live there)
|
|
260
|
+
- `~/.agents/skills/*` — cross-agent dir; also where `skills.sh` installs
|
|
261
|
+
|
|
262
|
+
`skman migrate` walks those locations, looks for `SKILL.md` dirs that
|
|
263
|
+
aren't already managed by skman, and adopts them:
|
|
264
|
+
|
|
265
|
+
- Reads `~/.agents/.skill-lock.json` (skills.sh v3) when present and uses
|
|
266
|
+
the recorded `sourceUrl` — your `npx skills` installs become git sources
|
|
267
|
+
tracked by skman, deduplicating skills that share a repo.
|
|
268
|
+
- Else, if the skill lives inside a git checkout, registers the enclosing
|
|
269
|
+
repo as a git source via its `origin`.
|
|
270
|
+
- Else, copies the skill into `~/.skman/imported/<name>/` and registers
|
|
271
|
+
that as a local source.
|
|
272
|
+
|
|
273
|
+
`skman migrate` refuses to overwrite skills you may have edited locally:
|
|
274
|
+
|
|
275
|
+
- **In a git checkout** with uncommitted changes or unpushed commits →
|
|
276
|
+
skipped. Commit + push upstream, then re-run.
|
|
277
|
+
- **In `~/.agents/skills/` with a `skillFolderHash`** in
|
|
278
|
+
`.skill-lock.json` (skills.sh v3) → the local folder's git tree SHA-1
|
|
279
|
+
is recomputed and compared. A mismatch means the folder was edited
|
|
280
|
+
after install; skman skips it. (Macros: `.DS_Store`, `__pycache__`,
|
|
281
|
+
`.git`, `node_modules` are filtered to avoid false positives.)
|
|
282
|
+
|
|
283
|
+
In both cases skman tells you which skill, where it lives, and why —
|
|
284
|
+
then leaves it alone. Resolve manually (commit/push, or revert your
|
|
285
|
+
edits, or just don't manage it with skman) and re-run.
|
|
286
|
+
|
|
287
|
+
After migration, skman manages the skill via its own suffixed symlinks
|
|
288
|
+
(`brainstorming-ab12cd`) and removes the original loose copy so the host
|
|
289
|
+
agent doesn't see both.
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
skman migrate --dry-run # preview what would happen
|
|
293
|
+
skman migrate # interactive (asks for confirmation)
|
|
294
|
+
skman migrate --yes # non-interactive
|
|
295
|
+
skman migrate --keep-originals # don't remove the on-disk copies after import
|
|
296
|
+
skman migrate --scan ~/elsewhere # scan an additional dir (repeatable)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`skman setup` runs `install-hook --write` followed by `migrate` and is the
|
|
300
|
+
recommended first-run command.
|
|
301
|
+
|
|
302
|
+
## Commands
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
skman paths
|
|
306
|
+
skman setup [--yes] [--keep-originals]
|
|
307
|
+
skman migrate [--dry-run] [--yes] [--keep-originals] [--scan PATH]
|
|
308
|
+
skman source add <url> [skills-to-enable] | remove <slug-or-url> | list
|
|
309
|
+
skman sync [--source NAME | --skill SLUG]
|
|
310
|
+
skman list
|
|
311
|
+
skman refresh
|
|
312
|
+
skman enable <skill>
|
|
313
|
+
skman disable <skill>
|
|
314
|
+
skman stats [--days N] [--skill SLUG]
|
|
315
|
+
skman hook
|
|
316
|
+
skman install-hook [--write]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`skills-to-enable` is an optional comma-separated whitelist of skill
|
|
320
|
+
slugs. When set, only those skills are enabled after sync; the rest are
|
|
321
|
+
recorded in state but left disabled (no symlink). Examples:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
skman source add https://github.com/obra/superpowers.git # enable everything in the source
|
|
325
|
+
skman source add https://github.com/obra/superpowers.git brainstorming,tdd
|
|
326
|
+
# enable only those two; others stay disabled
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Environment overrides (advanced)
|
|
330
|
+
|
|
331
|
+
- `SKMAN_ROOT` — state dir (default `~/.skman`)
|
|
332
|
+
- `SKMAN_TARGET_DIRS` — colon-separated list of agent skill dirs
|
|
333
|
+
(default `~/.agents/skills:~/.claude/skills`). Mainly used by tests.
|
|
334
|
+
- `SKMAN_GITHUB_MIRROR` — rewrite GitHub clone URLs through a mirror
|
|
335
|
+
(useful in regions where `github.com` is slow or blocked). Two forms:
|
|
336
|
+
- **Hostname** (e.g. `hub.fastgit.org`) — replaces `github.com` in
|
|
337
|
+
the URL. `git@github.com:o/r` is converted to HTTPS first, so SSH
|
|
338
|
+
sources work too.
|
|
339
|
+
- **Full URL** (e.g. `https://ghproxy.com`) — treated as a prefix;
|
|
340
|
+
the original `https://github.com/o/r` URL is appended.
|
|
341
|
+
The original `url` recorded in `state.json` is unchanged; the mirror
|
|
342
|
+
only applies at clone/fetch time, and sync prints the rewritten URL.
|
|
343
|
+
|
|
344
|
+
## Publishing (maintainers)
|
|
345
|
+
|
|
346
|
+
The version is read from `skman/__init__.py` (`__version__`). Bump it,
|
|
347
|
+
commit, then build and upload:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# 1. Bump skman/__init__.py __version__ and commit
|
|
351
|
+
# 2. Tag the release (optional but recommended)
|
|
352
|
+
git tag v$(python3 -c "import skman; print(skman.__version__)")
|
|
353
|
+
git push --tags
|
|
354
|
+
|
|
355
|
+
# 3. Build
|
|
356
|
+
python3 -m pip install --upgrade build twine
|
|
357
|
+
rm -rf dist/ && python3 -m build # produces dist/skman-X.Y.Z-py3-none-any.whl and .tar.gz
|
|
358
|
+
|
|
359
|
+
# 4. Sanity-check the artifacts
|
|
360
|
+
python3 -m twine check dist/*
|
|
361
|
+
|
|
362
|
+
# 5. Upload to TestPyPI first, then PyPI
|
|
363
|
+
python3 -m twine upload --repository testpypi dist/*
|
|
364
|
+
python3 -m twine upload dist/*
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Configure credentials in `~/.pypirc` (or use API tokens via
|
|
368
|
+
`TWINE_USERNAME=__token__ TWINE_PASSWORD=<pypi-token>`).
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
[MIT](LICENSE).
|