modern-python-guidance 0.2.3__tar.gz → 0.3.1__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.
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/CHANGELOG.md +30 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/PKG-INFO +41 -20
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/README.md +40 -19
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/pyproject.toml +1 -1
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/__init__.py +1 -1
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/cli.py +55 -0
- modern_python_guidance-0.3.1/src/modern_python_guidance/setup_cmd.py +186 -0
- modern_python_guidance-0.3.1/src/modern_python_guidance/uninstall_cmd.py +171 -0
- modern_python_guidance-0.3.1/tests/test_setup.py +409 -0
- modern_python_guidance-0.3.1/tests/test_uninstall.py +371 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/.github/workflows/ci.yml +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/.github/workflows/publish.yml +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/.gitignore +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/CONTRIBUTING.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/LICENSE +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/LICENSE-MIT +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/SECURITY.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/pyproject.toml +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/app.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/config.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/crawler.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/models.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/scanner.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-modern/src/utils.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/pyproject.toml +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/setup.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/app.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/config.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/crawler.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/models.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/scanner.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-a-outdated/src/utils.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-b-modern/myapp/models.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-b-modern/myapp/views.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-b-outdated/myapp/models.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-b-outdated/myapp/views.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-c-modern/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/fixtures/variant-c-outdated/tests/test_calculator.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/mcp-config.json +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v2.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v3-mcp.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v3.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v4-a.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v4-b.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt-v4-c.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/prompt.txt +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/run-mcp.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/run-v4.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/run.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/score-v2.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/score-v3.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/score-v4.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/score.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/bench/test-scorer.sh +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/docs/benchmark-evaluation.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/docs/benchmark-procedure.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/docs/design.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/SKILL.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/__main__.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/compat.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/frontmatter.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/guide_index.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/mcp_server.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/retrieve.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/search.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/version_detect.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_cli_integration.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_frontmatter.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_mcp_server.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_retrieve.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_search.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_skill_sync.py +0 -0
- {modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/tests/test_version_detect.py +0 -0
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.1] — 2026-05-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `mpg uninstall` command: reverses `mpg setup` by deregistering the MCP server and removing the Agent Skills symlink in one command (closes #63)
|
|
10
|
+
- CLI flags: `--mcp-only`, `--skills-only`, `--project-dir`, `--dry-run` (no `--scope`; uninstall clears every scope `setup` can write to)
|
|
11
|
+
- Per-scope MCP deregistration (`claude mcp remove -s local` and `-s user`): a live probe showed `claude mcp remove` without a scope removes nothing when the server is registered in multiple scopes, so uninstall enumerates scopes explicitly to avoid leaving residue
|
|
12
|
+
- Symlink-only removal safety: only the symlink mpg created is removed (never its target), a non-symlink entity at the link path is refused, dangling symlinks are removed, and the parent `.claude/skills/` directory is preserved
|
|
13
|
+
- 26 new tests (V-015 through V-031)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Extracted shared `_skills_link_path` helper in `setup_cmd` so `setup` and `uninstall` resolve the Skills symlink location identically (no drift)
|
|
18
|
+
|
|
19
|
+
## [0.3.0] — 2026-05-28
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `mpg setup` command: one-command MCP server registration + Agent Skills symlink creation. Replaces 3-4 manual steps with `pip install modern-python-guidance && mpg setup` (closes #60)
|
|
24
|
+
- CLI flags: `--mcp-only`, `--skills-only`, `--scope {user,local}`, `--project-dir`, `--dry-run`
|
|
25
|
+
- Project root auto-detection (`.claude/` → `.git/` → `pyproject.toml` upward search) for correct Skills symlink placement from subdirectories
|
|
26
|
+
- Idempotent operation: re-running `mpg setup` skips already-correct state, replaces stale/broken symlinks, errors on non-symlink blockers
|
|
27
|
+
- Partial success handling: MCP and Skills run independently; one failure does not block the other
|
|
28
|
+
- 33 new tests for setup command (V-001 through V-014 verification points)
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- README Quick Start: reduced from 3 code blocks to 2 lines (`pip install` + `mpg setup`). Manual setup moved to collapsible `<details>` section
|
|
33
|
+
|
|
5
34
|
## [0.2.3] — 2026-05-28
|
|
6
35
|
|
|
7
36
|
### Fixed
|
|
@@ -88,6 +117,7 @@ Initial release.
|
|
|
88
117
|
- Strict YAML-subset frontmatter parser (no PyYAML dependency)
|
|
89
118
|
- GitHub Actions CI (pytest + ruff on Python 3.11, 3.12, 3.13)
|
|
90
119
|
|
|
120
|
+
[0.3.0]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.3.0
|
|
91
121
|
[0.2.3]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.3
|
|
92
122
|
[0.2.2]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.2
|
|
93
123
|
[0.2.1]: https://github.com/yottayoshida/modern-python-guidance/releases/tag/v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modern-python-guidance
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yottayoshida/modern-python-guidance
|
|
6
6
|
Project-URL: Repository, https://github.com/yottayoshida/modern-python-guidance
|
|
@@ -50,15 +50,29 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 versio
|
|
|
50
50
|
|
|
51
51
|
## Quick start
|
|
52
52
|
|
|
53
|
-
###
|
|
53
|
+
### Claude Code (recommended)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
```bash
|
|
56
|
+
pip install modern-python-guidance
|
|
57
|
+
mpg setup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This registers the MCP server and links Agent Skills in one command. Start a new Claude Code session afterwards — newly registered MCP servers and skills take effect on the next launch.
|
|
61
|
+
|
|
62
|
+
### CLI
|
|
56
63
|
|
|
57
64
|
```bash
|
|
58
65
|
pip install modern-python-guidance
|
|
66
|
+
mpg search "pydantic validator"
|
|
67
|
+
mpg retrieve pydantic-v2-validators
|
|
59
68
|
```
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
`mpg` is the short alias for `modern-python-guidance`. Both work.
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary>Manual setup / other agents</summary>
|
|
74
|
+
|
|
75
|
+
**MCP registration (Claude Code):**
|
|
62
76
|
```bash
|
|
63
77
|
claude mcp add mpg -- mpg mcp
|
|
64
78
|
```
|
|
@@ -75,29 +89,36 @@ claude mcp add mpg -- mpg mcp
|
|
|
75
89
|
}
|
|
76
90
|
```
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
### CLI
|
|
81
|
-
|
|
92
|
+
**Agent Skills symlink (Claude Code):**
|
|
82
93
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Search for a pattern
|
|
86
|
-
mpg search "pydantic validator"
|
|
87
|
-
|
|
88
|
-
# Get the full guide
|
|
89
|
-
mpg retrieve pydantic-v2-validators
|
|
94
|
+
mpg setup --skills-only
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
**`mpg setup` flags:**
|
|
98
|
+
| Flag | Purpose |
|
|
99
|
+
|------|---------|
|
|
100
|
+
| `--mcp-only` | MCP registration only |
|
|
101
|
+
| `--skills-only` | Agent Skills symlink only |
|
|
102
|
+
| `--scope {user,local}` | MCP scope (default: user) |
|
|
103
|
+
| `--project-dir PATH` | Target project for Skills symlink |
|
|
104
|
+
| `--dry-run` | Show what would be done |
|
|
93
105
|
|
|
106
|
+
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
|
|
94
107
|
```bash
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
ln -s "$SKILL_DIR" your-project/.claude/skills/modern-python-guidance
|
|
108
|
+
mpg uninstall # remove both
|
|
109
|
+
mpg uninstall --dry-run # preview what would be removed
|
|
98
110
|
```
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
| Flag | Purpose |
|
|
113
|
+
|------|---------|
|
|
114
|
+
| `--mcp-only` | MCP deregistration only |
|
|
115
|
+
| `--skills-only` | Agent Skills unlink only |
|
|
116
|
+
| `--project-dir PATH` | Target project for the Skills symlink |
|
|
117
|
+
| `--dry-run` | Show what would be done |
|
|
118
|
+
|
|
119
|
+
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlink mpg created (never its target or other skills), and is idempotent — running it on an already-clean state is a harmless no-op.
|
|
120
|
+
|
|
121
|
+
</details>
|
|
101
122
|
|
|
102
123
|
## CLI usage
|
|
103
124
|
|
|
@@ -19,15 +19,29 @@ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 versio
|
|
|
19
19
|
|
|
20
20
|
## Quick start
|
|
21
21
|
|
|
22
|
-
###
|
|
22
|
+
### Claude Code (recommended)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
```bash
|
|
25
|
+
pip install modern-python-guidance
|
|
26
|
+
mpg setup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This registers the MCP server and links Agent Skills in one command. Start a new Claude Code session afterwards — newly registered MCP servers and skills take effect on the next launch.
|
|
30
|
+
|
|
31
|
+
### CLI
|
|
25
32
|
|
|
26
33
|
```bash
|
|
27
34
|
pip install modern-python-guidance
|
|
35
|
+
mpg search "pydantic validator"
|
|
36
|
+
mpg retrieve pydantic-v2-validators
|
|
28
37
|
```
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
`mpg` is the short alias for `modern-python-guidance`. Both work.
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary>Manual setup / other agents</summary>
|
|
43
|
+
|
|
44
|
+
**MCP registration (Claude Code):**
|
|
31
45
|
```bash
|
|
32
46
|
claude mcp add mpg -- mpg mcp
|
|
33
47
|
```
|
|
@@ -44,29 +58,36 @@ claude mcp add mpg -- mpg mcp
|
|
|
44
58
|
}
|
|
45
59
|
```
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### CLI
|
|
50
|
-
|
|
61
|
+
**Agent Skills symlink (Claude Code):**
|
|
51
62
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Search for a pattern
|
|
55
|
-
mpg search "pydantic validator"
|
|
56
|
-
|
|
57
|
-
# Get the full guide
|
|
58
|
-
mpg retrieve pydantic-v2-validators
|
|
63
|
+
mpg setup --skills-only
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
**`mpg setup` flags:**
|
|
67
|
+
| Flag | Purpose |
|
|
68
|
+
|------|---------|
|
|
69
|
+
| `--mcp-only` | MCP registration only |
|
|
70
|
+
| `--skills-only` | Agent Skills symlink only |
|
|
71
|
+
| `--scope {user,local}` | MCP scope (default: user) |
|
|
72
|
+
| `--project-dir PATH` | Target project for Skills symlink |
|
|
73
|
+
| `--dry-run` | Show what would be done |
|
|
62
74
|
|
|
75
|
+
**Uninstall** — reverse `mpg setup` (deregister the MCP server and unlink Agent Skills):
|
|
63
76
|
```bash
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
ln -s "$SKILL_DIR" your-project/.claude/skills/modern-python-guidance
|
|
77
|
+
mpg uninstall # remove both
|
|
78
|
+
mpg uninstall --dry-run # preview what would be removed
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
| Flag | Purpose |
|
|
82
|
+
|------|---------|
|
|
83
|
+
| `--mcp-only` | MCP deregistration only |
|
|
84
|
+
| `--skills-only` | Agent Skills unlink only |
|
|
85
|
+
| `--project-dir PATH` | Target project for the Skills symlink |
|
|
86
|
+
| `--dry-run` | Show what would be done |
|
|
87
|
+
|
|
88
|
+
`mpg uninstall` clears the MCP registration from every scope `setup` can write to (user and local), removes only the symlink mpg created (never its target or other skills), and is idempotent — running it on an already-clean state is a harmless no-op.
|
|
89
|
+
|
|
90
|
+
</details>
|
|
70
91
|
|
|
71
92
|
## CLI usage
|
|
72
93
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modern-python-guidance"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0 OR MIT"
|
{modern_python_guidance-0.2.3 → modern_python_guidance-0.3.1}/src/modern_python_guidance/cli.py
RENAMED
|
@@ -65,6 +65,32 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
65
65
|
# mcp
|
|
66
66
|
subparsers.add_parser("mcp", help="Start MCP server (JSON-RPC over stdio)")
|
|
67
67
|
|
|
68
|
+
# setup
|
|
69
|
+
p_setup = subparsers.add_parser(
|
|
70
|
+
"setup", help="Register MCP server and link Agent Skills",
|
|
71
|
+
)
|
|
72
|
+
p_setup.add_argument("--mcp-only", action="store_true", help="MCP registration only")
|
|
73
|
+
p_setup.add_argument("--skills-only", action="store_true", help="Skills symlink only")
|
|
74
|
+
p_setup.add_argument(
|
|
75
|
+
"--scope", choices=["user", "local"], default="user",
|
|
76
|
+
help="MCP scope (default: user)",
|
|
77
|
+
)
|
|
78
|
+
p_setup.add_argument(
|
|
79
|
+
"--project-dir", type=Path, help="Project directory for Skills symlink",
|
|
80
|
+
)
|
|
81
|
+
p_setup.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
82
|
+
|
|
83
|
+
# uninstall
|
|
84
|
+
p_uninstall = subparsers.add_parser(
|
|
85
|
+
"uninstall", help="Reverse 'setup': deregister MCP server and unlink Agent Skills",
|
|
86
|
+
)
|
|
87
|
+
p_uninstall.add_argument("--mcp-only", action="store_true", help="MCP deregistration only")
|
|
88
|
+
p_uninstall.add_argument("--skills-only", action="store_true", help="Skills unlink only")
|
|
89
|
+
p_uninstall.add_argument(
|
|
90
|
+
"--project-dir", type=Path, help="Project directory for Skills symlink",
|
|
91
|
+
)
|
|
92
|
+
p_uninstall.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
93
|
+
|
|
68
94
|
args = parser.parse_args(argv)
|
|
69
95
|
|
|
70
96
|
if args.command is None:
|
|
@@ -86,6 +112,10 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
86
112
|
_cmd_detect_version(args)
|
|
87
113
|
elif args.command == "mcp":
|
|
88
114
|
_cmd_mcp()
|
|
115
|
+
elif args.command == "setup":
|
|
116
|
+
_cmd_setup(args)
|
|
117
|
+
elif args.command == "uninstall":
|
|
118
|
+
_cmd_uninstall(args)
|
|
89
119
|
except BrokenPipeError:
|
|
90
120
|
sys.exit(0)
|
|
91
121
|
|
|
@@ -215,3 +245,28 @@ def _cmd_mcp() -> None:
|
|
|
215
245
|
from modern_python_guidance.mcp_server import serve
|
|
216
246
|
|
|
217
247
|
serve()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _cmd_setup(args: argparse.Namespace) -> None:
|
|
251
|
+
from modern_python_guidance.setup_cmd import run_setup
|
|
252
|
+
|
|
253
|
+
code = run_setup(
|
|
254
|
+
scope=args.scope,
|
|
255
|
+
mcp_only=args.mcp_only,
|
|
256
|
+
skills_only=args.skills_only,
|
|
257
|
+
project_dir=args.project_dir,
|
|
258
|
+
dry_run=args.dry_run,
|
|
259
|
+
)
|
|
260
|
+
sys.exit(code)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cmd_uninstall(args: argparse.Namespace) -> None:
|
|
264
|
+
from modern_python_guidance.uninstall_cmd import run_uninstall
|
|
265
|
+
|
|
266
|
+
code = run_uninstall(
|
|
267
|
+
mcp_only=args.mcp_only,
|
|
268
|
+
skills_only=args.skills_only,
|
|
269
|
+
project_dir=args.project_dir,
|
|
270
|
+
dry_run=args.dry_run,
|
|
271
|
+
)
|
|
272
|
+
sys.exit(code)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Automate MCP server registration and Agent Skills symlink creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SKILLS_LINK_NAME = "modern-python-guidance"
|
|
14
|
+
MCP_SERVER_NAME = "mpg"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_skills_dir() -> Path:
|
|
18
|
+
"""Resolve the bundled skills directory (package install or editable)."""
|
|
19
|
+
try:
|
|
20
|
+
pkg = importlib.resources.files("modern_python_guidance") / "skills"
|
|
21
|
+
skills_path = Path(str(pkg)) / SKILLS_LINK_NAME
|
|
22
|
+
if skills_path.is_dir():
|
|
23
|
+
return skills_path
|
|
24
|
+
except (TypeError, FileNotFoundError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
src_root = Path(__file__).resolve().parent.parent.parent
|
|
28
|
+
dev_path = src_root / "skills" / SKILLS_LINK_NAME
|
|
29
|
+
if dev_path.is_dir():
|
|
30
|
+
return dev_path
|
|
31
|
+
|
|
32
|
+
msg = "Cannot locate bundled skills directory"
|
|
33
|
+
raise FileNotFoundError(msg)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _find_project_root(start: Path | None = None) -> Path:
|
|
37
|
+
"""Walk upward from *start* to find the project root."""
|
|
38
|
+
current = (start or Path.cwd()).resolve()
|
|
39
|
+
markers = [".claude", ".git", "pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
for marker in markers:
|
|
42
|
+
d = current
|
|
43
|
+
while True:
|
|
44
|
+
candidate = d / marker
|
|
45
|
+
if candidate.exists():
|
|
46
|
+
return d
|
|
47
|
+
parent = d.parent
|
|
48
|
+
if parent == d:
|
|
49
|
+
break
|
|
50
|
+
d = parent
|
|
51
|
+
|
|
52
|
+
return current
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _skills_link_path(project_dir: Path | None = None) -> Path:
|
|
56
|
+
"""Resolve the Agent Skills symlink path: ``<root>/.claude/skills/<name>``.
|
|
57
|
+
|
|
58
|
+
Single source of truth for where the skills symlink lives, shared by
|
|
59
|
+
``setup_skills`` (creation) and ``uninstall_skills`` (removal) so the two
|
|
60
|
+
operations cannot drift in how they locate the link.
|
|
61
|
+
"""
|
|
62
|
+
root = project_dir or _find_project_root()
|
|
63
|
+
return root / ".claude" / "skills" / SKILLS_LINK_NAME
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def setup_mcp(
|
|
67
|
+
*,
|
|
68
|
+
scope: str = "user",
|
|
69
|
+
dry_run: bool = False,
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""Register the MCP server with Claude Code. Returns True on success."""
|
|
72
|
+
args = ["mcp", "add", "--scope", scope, MCP_SERVER_NAME, "--", "mpg", "mcp"]
|
|
73
|
+
|
|
74
|
+
if dry_run:
|
|
75
|
+
print(f"Would run: claude {' '.join(args)}")
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
claude = shutil.which("claude")
|
|
79
|
+
if claude is None:
|
|
80
|
+
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
81
|
+
print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
|
|
82
|
+
print(
|
|
83
|
+
"Run 'mpg setup --skills-only' to set up Agent Skills without MCP.",
|
|
84
|
+
file=sys.stderr,
|
|
85
|
+
)
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
cmd = [claude, *args]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
|
92
|
+
except subprocess.TimeoutExpired:
|
|
93
|
+
print("Error: 'claude mcp add' timed out after 30 seconds.", file=sys.stderr)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
if result.returncode != 0:
|
|
97
|
+
stderr_text = result.stderr.decode(errors="replace").strip()
|
|
98
|
+
print(f"Error: 'claude mcp add' failed (exit {result.returncode}).", file=sys.stderr)
|
|
99
|
+
if stderr_text:
|
|
100
|
+
print(stderr_text, file=sys.stderr)
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
print(f"MCP server registered with Claude Code ({scope} scope).")
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def setup_skills(
|
|
108
|
+
*,
|
|
109
|
+
project_dir: Path | None = None,
|
|
110
|
+
dry_run: bool = False,
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Create Agent Skills symlink. Returns True on success."""
|
|
113
|
+
try:
|
|
114
|
+
source = _find_skills_dir()
|
|
115
|
+
except FileNotFoundError as e:
|
|
116
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
root = project_dir or _find_project_root()
|
|
120
|
+
link_path = _skills_link_path(project_dir)
|
|
121
|
+
skills_parent = link_path.parent
|
|
122
|
+
|
|
123
|
+
if dry_run:
|
|
124
|
+
print(f"Would link: {link_path} -> {source}")
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
if link_path.is_symlink():
|
|
128
|
+
current_target = Path(os.readlink(link_path))
|
|
129
|
+
if current_target == source or link_path.resolve() == source.resolve():
|
|
130
|
+
print(f"Agent Skills already linked at {link_path.relative_to(root)}")
|
|
131
|
+
return True
|
|
132
|
+
# Stale or broken symlink — replace
|
|
133
|
+
link_path.unlink()
|
|
134
|
+
elif link_path.exists():
|
|
135
|
+
print(
|
|
136
|
+
f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
|
|
137
|
+
file=sys.stderr,
|
|
138
|
+
)
|
|
139
|
+
print(
|
|
140
|
+
f"Remove it manually: rm -rf {shlex.quote(str(link_path))}",
|
|
141
|
+
file=sys.stderr,
|
|
142
|
+
)
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
skills_parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
os.symlink(source, link_path)
|
|
148
|
+
except OSError as e:
|
|
149
|
+
print(f"Error creating symlink: {e}", file=sys.stderr)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
print(f"Agent Skills linked to {link_path.relative_to(root)}")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_setup(
|
|
157
|
+
*,
|
|
158
|
+
scope: str = "user",
|
|
159
|
+
mcp_only: bool = False,
|
|
160
|
+
skills_only: bool = False,
|
|
161
|
+
project_dir: Path | None = None,
|
|
162
|
+
dry_run: bool = False,
|
|
163
|
+
) -> int:
|
|
164
|
+
"""Run the full setup sequence. Returns exit code (0=success, 1=failure)."""
|
|
165
|
+
if mcp_only and skills_only:
|
|
166
|
+
print("Error: --mcp-only and --skills-only are mutually exclusive.", file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
do_mcp = not skills_only
|
|
170
|
+
do_skills = not mcp_only
|
|
171
|
+
|
|
172
|
+
mcp_ok = True
|
|
173
|
+
skills_ok = True
|
|
174
|
+
|
|
175
|
+
if do_mcp:
|
|
176
|
+
mcp_ok = setup_mcp(scope=scope, dry_run=dry_run)
|
|
177
|
+
|
|
178
|
+
if do_skills:
|
|
179
|
+
skills_ok = setup_skills(project_dir=project_dir, dry_run=dry_run)
|
|
180
|
+
|
|
181
|
+
if mcp_ok and skills_ok:
|
|
182
|
+
if not dry_run and do_mcp and do_skills:
|
|
183
|
+
print("Ready. Start Claude Code to use mpg guides.")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
return 1
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Reverse `mpg setup`: deregister the MCP server and remove the Skills symlink."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from modern_python_guidance.setup_cmd import (
|
|
12
|
+
MCP_SERVER_NAME,
|
|
13
|
+
_find_project_root,
|
|
14
|
+
_skills_link_path,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# The scopes `mpg setup --scope {user,local}` can write to. uninstall clears
|
|
18
|
+
# both because it does not track which scope setup used. Deterministic
|
|
19
|
+
# enumeration is required: `claude mcp remove <name>` WITHOUT a scope does NOT
|
|
20
|
+
# remove when the server exists in multiple scopes — it just prints per-scope
|
|
21
|
+
# hints (exit 0), removing nothing. Per-scope removal avoids that ambiguity.
|
|
22
|
+
_REMOVE_SCOPES = ("local", "user")
|
|
23
|
+
|
|
24
|
+
# Substring printed by `claude mcp remove <name> -s <scope>` when the server is
|
|
25
|
+
# NOT present in that scope, e.g. "No user-scoped MCP server found with name...".
|
|
26
|
+
# Per-scope removal returns exit 0 whether it removed or found nothing, so this
|
|
27
|
+
# marker is how we tell "removed something" from "was already absent". Matching
|
|
28
|
+
# the stable middle of the phrase (not the scope word or quoted name) keeps it
|
|
29
|
+
# robust; if the wording changes we over-report "removed", never falsely claim
|
|
30
|
+
# clean while leaving residue.
|
|
31
|
+
_NOT_IN_SCOPE_MARKER = "-scoped MCP server found"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def uninstall_mcp(*, dry_run: bool = False) -> bool:
|
|
35
|
+
"""Deregister the MCP server from Claude Code. Returns True on success.
|
|
36
|
+
|
|
37
|
+
Removes the server from every scope `mpg setup` can write to (user, local),
|
|
38
|
+
since the scope used at setup time is not tracked. Idempotent: scopes where
|
|
39
|
+
the server is absent are a no-op.
|
|
40
|
+
"""
|
|
41
|
+
if dry_run:
|
|
42
|
+
for scope in _REMOVE_SCOPES:
|
|
43
|
+
print(f"Would run: claude mcp remove {MCP_SERVER_NAME} -s {scope}")
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
claude = shutil.which("claude")
|
|
47
|
+
if claude is None:
|
|
48
|
+
print("Error: 'claude' command not found.", file=sys.stderr)
|
|
49
|
+
print("Install Claude Code: https://claude.ai/download", file=sys.stderr)
|
|
50
|
+
print(
|
|
51
|
+
"Run 'mpg uninstall --skills-only' to remove Agent Skills without MCP.",
|
|
52
|
+
file=sys.stderr,
|
|
53
|
+
)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
removed_any = False
|
|
57
|
+
for scope in _REMOVE_SCOPES:
|
|
58
|
+
cmd = [claude, "mcp", "remove", MCP_SERVER_NAME, "-s", scope]
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
|
61
|
+
except subprocess.TimeoutExpired:
|
|
62
|
+
print(
|
|
63
|
+
f"Error: 'claude mcp remove -s {scope}' timed out after 30 seconds.",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
return False
|
|
67
|
+
except OSError as e:
|
|
68
|
+
# `claude` resolved on PATH but could not be executed (broken binary,
|
|
69
|
+
# permissions, platform quirk). Fail gracefully instead of crashing.
|
|
70
|
+
print(f"Error: failed to run 'claude mcp remove -s {scope}': {e}", file=sys.stderr)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
output = (result.stdout + result.stderr).decode(errors="replace").strip()
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
# A genuine failure (permissions, broken CLI, etc.) — do not hide it.
|
|
76
|
+
print(
|
|
77
|
+
f"Error: 'claude mcp remove -s {scope}' failed (exit {result.returncode}).",
|
|
78
|
+
file=sys.stderr,
|
|
79
|
+
)
|
|
80
|
+
if output:
|
|
81
|
+
print(output, file=sys.stderr)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
if _NOT_IN_SCOPE_MARKER not in output:
|
|
85
|
+
removed_any = True
|
|
86
|
+
|
|
87
|
+
if removed_any:
|
|
88
|
+
print("MCP server removed from Claude Code.")
|
|
89
|
+
else:
|
|
90
|
+
print("MCP server not registered — nothing to remove.")
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def uninstall_skills(
|
|
95
|
+
*,
|
|
96
|
+
project_dir: Path | None = None,
|
|
97
|
+
dry_run: bool = False,
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Remove the Agent Skills symlink. Returns True on success.
|
|
100
|
+
|
|
101
|
+
Safety: only a symlink is removed (`Path.unlink` deletes the link entry,
|
|
102
|
+
never the target). A non-symlink entity at the link path is refused, not
|
|
103
|
+
deleted. The parent `.claude/skills/` directory is left intact.
|
|
104
|
+
|
|
105
|
+
Idempotent: if no symlink is present, this is a no-op success.
|
|
106
|
+
"""
|
|
107
|
+
root = project_dir or _find_project_root()
|
|
108
|
+
link_path = _skills_link_path(project_dir)
|
|
109
|
+
|
|
110
|
+
# Primary gate: is_symlink() is True even for a dangling (broken) symlink,
|
|
111
|
+
# whereas exists() is False for one. We must remove dangling links too.
|
|
112
|
+
if not link_path.is_symlink():
|
|
113
|
+
if link_path.exists():
|
|
114
|
+
# A real file/dir lives here — not ours. Refuse to delete it.
|
|
115
|
+
print(
|
|
116
|
+
f"Error: {link_path.relative_to(root)} exists and is not a symlink.",
|
|
117
|
+
file=sys.stderr,
|
|
118
|
+
)
|
|
119
|
+
print(
|
|
120
|
+
f"Remove it manually: rm -rf {shlex.quote(str(link_path))}",
|
|
121
|
+
file=sys.stderr,
|
|
122
|
+
)
|
|
123
|
+
return False
|
|
124
|
+
# Nothing linked — already clean.
|
|
125
|
+
print(f"Agent Skills not linked at {link_path.relative_to(root)} — nothing to remove.")
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
if dry_run:
|
|
129
|
+
print(f"Would remove: {link_path}")
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
link_path.unlink()
|
|
134
|
+
except OSError as e:
|
|
135
|
+
print(f"Error removing symlink: {e}", file=sys.stderr)
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
print(f"Agent Skills unlinked from {link_path.relative_to(root)}")
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_uninstall(
|
|
143
|
+
*,
|
|
144
|
+
mcp_only: bool = False,
|
|
145
|
+
skills_only: bool = False,
|
|
146
|
+
project_dir: Path | None = None,
|
|
147
|
+
dry_run: bool = False,
|
|
148
|
+
) -> int:
|
|
149
|
+
"""Run the full uninstall sequence. Returns exit code (0=success, 1=failure)."""
|
|
150
|
+
if mcp_only and skills_only:
|
|
151
|
+
print("Error: --mcp-only and --skills-only are mutually exclusive.", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
do_mcp = not skills_only
|
|
155
|
+
do_skills = not mcp_only
|
|
156
|
+
|
|
157
|
+
mcp_ok = True
|
|
158
|
+
skills_ok = True
|
|
159
|
+
|
|
160
|
+
if do_mcp:
|
|
161
|
+
mcp_ok = uninstall_mcp(dry_run=dry_run)
|
|
162
|
+
|
|
163
|
+
if do_skills:
|
|
164
|
+
skills_ok = uninstall_skills(project_dir=project_dir, dry_run=dry_run)
|
|
165
|
+
|
|
166
|
+
if mcp_ok and skills_ok:
|
|
167
|
+
if not dry_run and do_mcp and do_skills:
|
|
168
|
+
print("Done. mpg has been removed.")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
return 1
|