fixpm 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.
- fixpm-0.1.0/.github/workflows/ci.yml +27 -0
- fixpm-0.1.0/.gitignore +13 -0
- fixpm-0.1.0/LICENSE +21 -0
- fixpm-0.1.0/PKG-INFO +199 -0
- fixpm-0.1.0/README.md +151 -0
- fixpm-0.1.0/pyproject.toml +52 -0
- fixpm-0.1.0/src/fixpm/__init__.py +3 -0
- fixpm-0.1.0/src/fixpm/__main__.py +3 -0
- fixpm-0.1.0/src/fixpm/cli.py +132 -0
- fixpm-0.1.0/src/fixpm/corrector.py +118 -0
- fixpm-0.1.0/src/fixpm/detector.py +223 -0
- fixpm-0.1.0/src/fixpm/distance.py +63 -0
- fixpm-0.1.0/src/fixpm/interactive.py +28 -0
- fixpm-0.1.0/src/fixpm/packages.py +204 -0
- fixpm-0.1.0/src/fixpm/rules/__init__.py +6 -0
- fixpm-0.1.0/src/fixpm/rules/base.py +109 -0
- fixpm-0.1.0/src/fixpm/rules/npm.py +97 -0
- fixpm-0.1.0/src/fixpm/rules/npx.py +29 -0
- fixpm-0.1.0/src/fixpm/rules/pnpm.py +58 -0
- fixpm-0.1.0/src/fixpm/rules/yarn.py +48 -0
- fixpm-0.1.0/src/fixpm/shell/__init__.py +1 -0
- fixpm-0.1.0/src/fixpm/shell/fixpm.bash +79 -0
- fixpm-0.1.0/src/fixpm/shell/fixpm.zsh +34 -0
- fixpm-0.1.0/tests/conftest.py +39 -0
- fixpm-0.1.0/tests/test_corrections.py +70 -0
- fixpm-0.1.0/tests/test_distance.py +40 -0
- fixpm-0.1.0/tests/test_packages.py +89 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
- name: Lint (ruff)
|
|
25
|
+
run: ruff check .
|
|
26
|
+
- name: Test (pytest)
|
|
27
|
+
run: pytest -q
|
fixpm-0.1.0/.gitignore
ADDED
fixpm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fixpm contributors
|
|
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.
|
fixpm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fixpm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive fixer for broken npm / npx / pnpm / yarn commands.
|
|
5
|
+
Author: fixpm contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 fixpm contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: cli,did-you-mean,npm,npx,pnpm,typo,yarn
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: POSIX
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Topic :: Software Development
|
|
40
|
+
Classifier: Topic :: Utilities
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: questionary>=2.0
|
|
43
|
+
Requires-Dist: typer>=0.12
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# fixpm
|
|
50
|
+
|
|
51
|
+
<!-- TODO(badges): CI status, PyPI version, Python versions, license -->
|
|
52
|
+
|
|
53
|
+
**Interactive command fixer for npm / npx / pnpm / yarn typos.**
|
|
54
|
+
|
|
55
|
+
You typed `npm isntall react`. Your terminal yelled at you. `fixpm` knows what you meant — pick the fix with arrow keys, press Enter, done.
|
|
56
|
+
|
|
57
|
+
## Why
|
|
58
|
+
|
|
59
|
+
Package-manager CLIs fail in very predictable ways: a mistyped subcommand (`isntall`), a missing flag prefix (`save-dev` instead of `--save-dev`), or a mistyped package name (`loadash`). Generic "did you mean" tools don't understand that `add` is valid for `yarn` but means `install` on `npm`, or that `--frozen-lockfile` belongs to `pnpm install`.
|
|
60
|
+
|
|
61
|
+
`fixpm` ships a declarative rule table per package manager plus live npm-registry lookups, so suggestions are syntax-aware, not just string-similar.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Automatic detection** — a tiny zsh/bash hook notices when an `npm`-family command exits non-zero and nudges you.
|
|
66
|
+
- **Five error classes**: subcommand typo, flag typo, missing flag prefix (`--`), missing required argument, package-name typo.
|
|
67
|
+
- **Registry-backed package fixes** — candidates ranked by edit distance *and* weekly download count (typos like `loadash` → `lodash`, never a dead lookalike package).
|
|
68
|
+
- **Interactive or one-shot** — arrow-key selection, or `--dry-run` to print fixes.
|
|
69
|
+
- **Extensible rule tables** — adding a manager or command is editing a data table, not writing code.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pipx install fixpm # recommended
|
|
75
|
+
# or
|
|
76
|
+
pip install --user fixpm
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> Note: verify the `fixpm` name is free on PyPI before your first release; if taken, rename the distribution in `pyproject.toml` (`name = ...`) — the console command can stay `fixpm`.
|
|
80
|
+
|
|
81
|
+
## Setup
|
|
82
|
+
|
|
83
|
+
Enable automatic failure detection (zsh):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
echo 'eval "$(fixpm --init zsh)"' >> ~/.zshrc
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
or bash:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
echo 'eval "$(fixpm --init bash)"' >> ~/.bashrc
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
No hook? Works manually too:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
fixpm "npm isntall react" # quote multi-token commands
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Demo
|
|
102
|
+
|
|
103
|
+
Text walkthrough of the interaction:
|
|
104
|
+
|
|
105
|
+
```console
|
|
106
|
+
$ npm isntall react --save-dev
|
|
107
|
+
npm ERR! Unknown command: "isntall"
|
|
108
|
+
|
|
109
|
+
↯ fix available — run fixpm ← printed automatically by the hook
|
|
110
|
+
|
|
111
|
+
$ fixpm
|
|
112
|
+
? Apply a fix: (↑/↓ move · enter select · ctrl+c cancel)
|
|
113
|
+
❯ npm install react --save-dev · command fix · 95%
|
|
114
|
+
Skip — do nothing
|
|
115
|
+
|
|
116
|
+
→ npm install react --save-dev ← executed after Enter
|
|
117
|
+
added 42 packages in 3s
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
<!-- TODO(demo): replace with an asciinema recording / GIF once published.
|
|
121
|
+
 -->
|
|
122
|
+
|
|
123
|
+
### What it catches
|
|
124
|
+
|
|
125
|
+
| You type | fixpm suggests | Class |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `npm isntall react` | `npm install react` | subcommand typo |
|
|
128
|
+
| `npm i loadash` | `npm i lodash` | package typo (registry-checked) |
|
|
129
|
+
| `npm install express save-dev` | `npm install express --save-dev` | missing flag prefix |
|
|
130
|
+
| `npm run` | `npm run <script>` | missing argument (not auto-executed) |
|
|
131
|
+
| `pnpm ad left-pad` | `pnpm add left-pad` | subcommand typo |
|
|
132
|
+
| `yarn globla add vite` | `yarn global add vite` | subcommand typo |
|
|
133
|
+
| `npx creat-react-app web` | `npx create-react-app web` | package typo |
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
1. The shell hook exports `$FIXPM_LAST_COMMAND` / `$FIXPM_LAST_EXIT_CODE`; `fixpm` reads them (or takes the command as arguments).
|
|
138
|
+
2. `detector.py` tokenizes the line, finds the manager binary, and classifies each issue against that manager's rule table.
|
|
139
|
+
3. `corrector.py` turns issues into concrete commands; package typos go through `packages.py`, which queries the npm registry search API and ranks by `0.62 × edit-distance similarity + 0.38 × log(weekly downloads)` (clamped). Network failures degrade gracefully to a small curated typo map — the tool never crashes offline.
|
|
140
|
+
|
|
141
|
+
## Troubleshooting
|
|
142
|
+
|
|
143
|
+
**The hook never prints a hint after a failed npm command.**
|
|
144
|
+
|
|
145
|
+
1. Make sure the probe itself finds suggestions:
|
|
146
|
+
```bash
|
|
147
|
+
fixpm --dry-run "npm isntall react" # must exit 0 and print fixes
|
|
148
|
+
```
|
|
149
|
+
If it prints `No fix found`, the typo simply isn't covered yet — the hook stays silent by design. PRs adding rules are welcome.
|
|
150
|
+
2. Make sure `fixpm` is actually on PATH *in that shell* (`command -v fixpm`). `pip install --user` puts it in Python's `Scripts` directory, which is often not on PATH — prefer `pipx`, or add the directory manually.
|
|
151
|
+
3. Still stuck? Re-run the failing command with `FIXPM_DEBUG=1` exported — the hook then shows why its probe failed instead of swallowing errors.
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
Contributions welcome — especially **rule tables** for pnpm/yarn coverage.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git clone https://github.com/Max-code7997/fixpm && cd fixpm
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
ruff check . && pytest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Adding a rule
|
|
164
|
+
|
|
165
|
+
Rules are data, not code. To teach `fixpm` a new command, alias, flag, or common misspelling, edit the relevant table in `src/fixpm/rules/`:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# src/fixpm/rules/npm.py
|
|
169
|
+
NPM = register(
|
|
170
|
+
ManagerSpec(
|
|
171
|
+
name="npm",
|
|
172
|
+
binaries=("npm",),
|
|
173
|
+
commands=("install", "run", "publish", ...), # canonical top-level commands
|
|
174
|
+
aliases={"i": "install", "innit": "init"}, # alt spelling -> canonical
|
|
175
|
+
flags={"install": ("--save-dev", "-D", ...)}, # validated flags per command
|
|
176
|
+
typo_hints={"isntall": "install"}, # deterministic overrides
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Guidelines:
|
|
182
|
+
|
|
183
|
+
- One PR per logical change (a new manager, a batch of flags, …).
|
|
184
|
+
- Add a pytest case in `tests/test_corrections.py` for every new behavior.
|
|
185
|
+
- Flag lists may be partial — unknown flags on unlisted commands are simply not validated (conservative by design).
|
|
186
|
+
|
|
187
|
+
See [CONTRIBUTING notes above](#contributing); open an issue first for new package managers.
|
|
188
|
+
|
|
189
|
+
## Roadmap
|
|
190
|
+
|
|
191
|
+
- [ ] Deeper pnpm / yarn (Berry) flag coverage
|
|
192
|
+
- [ ] fish & PowerShell hooks
|
|
193
|
+
- [ ] Validate `npm run <script>` against local `package.json`
|
|
194
|
+
- [ ] On-disk registry cache with TTL
|
|
195
|
+
- [ ] `fixpm --explain` (why this suggestion)
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
[MIT](LICENSE)
|
fixpm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# fixpm
|
|
2
|
+
|
|
3
|
+
<!-- TODO(badges): CI status, PyPI version, Python versions, license -->
|
|
4
|
+
|
|
5
|
+
**Interactive command fixer for npm / npx / pnpm / yarn typos.**
|
|
6
|
+
|
|
7
|
+
You typed `npm isntall react`. Your terminal yelled at you. `fixpm` knows what you meant — pick the fix with arrow keys, press Enter, done.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Package-manager CLIs fail in very predictable ways: a mistyped subcommand (`isntall`), a missing flag prefix (`save-dev` instead of `--save-dev`), or a mistyped package name (`loadash`). Generic "did you mean" tools don't understand that `add` is valid for `yarn` but means `install` on `npm`, or that `--frozen-lockfile` belongs to `pnpm install`.
|
|
12
|
+
|
|
13
|
+
`fixpm` ships a declarative rule table per package manager plus live npm-registry lookups, so suggestions are syntax-aware, not just string-similar.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Automatic detection** — a tiny zsh/bash hook notices when an `npm`-family command exits non-zero and nudges you.
|
|
18
|
+
- **Five error classes**: subcommand typo, flag typo, missing flag prefix (`--`), missing required argument, package-name typo.
|
|
19
|
+
- **Registry-backed package fixes** — candidates ranked by edit distance *and* weekly download count (typos like `loadash` → `lodash`, never a dead lookalike package).
|
|
20
|
+
- **Interactive or one-shot** — arrow-key selection, or `--dry-run` to print fixes.
|
|
21
|
+
- **Extensible rule tables** — adding a manager or command is editing a data table, not writing code.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pipx install fixpm # recommended
|
|
27
|
+
# or
|
|
28
|
+
pip install --user fixpm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> Note: verify the `fixpm` name is free on PyPI before your first release; if taken, rename the distribution in `pyproject.toml` (`name = ...`) — the console command can stay `fixpm`.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Enable automatic failure detection (zsh):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
echo 'eval "$(fixpm --init zsh)"' >> ~/.zshrc
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
or bash:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
echo 'eval "$(fixpm --init bash)"' >> ~/.bashrc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No hook? Works manually too:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
fixpm "npm isntall react" # quote multi-token commands
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Demo
|
|
54
|
+
|
|
55
|
+
Text walkthrough of the interaction:
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
$ npm isntall react --save-dev
|
|
59
|
+
npm ERR! Unknown command: "isntall"
|
|
60
|
+
|
|
61
|
+
↯ fix available — run fixpm ← printed automatically by the hook
|
|
62
|
+
|
|
63
|
+
$ fixpm
|
|
64
|
+
? Apply a fix: (↑/↓ move · enter select · ctrl+c cancel)
|
|
65
|
+
❯ npm install react --save-dev · command fix · 95%
|
|
66
|
+
Skip — do nothing
|
|
67
|
+
|
|
68
|
+
→ npm install react --save-dev ← executed after Enter
|
|
69
|
+
added 42 packages in 3s
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
<!-- TODO(demo): replace with an asciinema recording / GIF once published.
|
|
73
|
+
 -->
|
|
74
|
+
|
|
75
|
+
### What it catches
|
|
76
|
+
|
|
77
|
+
| You type | fixpm suggests | Class |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `npm isntall react` | `npm install react` | subcommand typo |
|
|
80
|
+
| `npm i loadash` | `npm i lodash` | package typo (registry-checked) |
|
|
81
|
+
| `npm install express save-dev` | `npm install express --save-dev` | missing flag prefix |
|
|
82
|
+
| `npm run` | `npm run <script>` | missing argument (not auto-executed) |
|
|
83
|
+
| `pnpm ad left-pad` | `pnpm add left-pad` | subcommand typo |
|
|
84
|
+
| `yarn globla add vite` | `yarn global add vite` | subcommand typo |
|
|
85
|
+
| `npx creat-react-app web` | `npx create-react-app web` | package typo |
|
|
86
|
+
|
|
87
|
+
## How it works
|
|
88
|
+
|
|
89
|
+
1. The shell hook exports `$FIXPM_LAST_COMMAND` / `$FIXPM_LAST_EXIT_CODE`; `fixpm` reads them (or takes the command as arguments).
|
|
90
|
+
2. `detector.py` tokenizes the line, finds the manager binary, and classifies each issue against that manager's rule table.
|
|
91
|
+
3. `corrector.py` turns issues into concrete commands; package typos go through `packages.py`, which queries the npm registry search API and ranks by `0.62 × edit-distance similarity + 0.38 × log(weekly downloads)` (clamped). Network failures degrade gracefully to a small curated typo map — the tool never crashes offline.
|
|
92
|
+
|
|
93
|
+
## Troubleshooting
|
|
94
|
+
|
|
95
|
+
**The hook never prints a hint after a failed npm command.**
|
|
96
|
+
|
|
97
|
+
1. Make sure the probe itself finds suggestions:
|
|
98
|
+
```bash
|
|
99
|
+
fixpm --dry-run "npm isntall react" # must exit 0 and print fixes
|
|
100
|
+
```
|
|
101
|
+
If it prints `No fix found`, the typo simply isn't covered yet — the hook stays silent by design. PRs adding rules are welcome.
|
|
102
|
+
2. Make sure `fixpm` is actually on PATH *in that shell* (`command -v fixpm`). `pip install --user` puts it in Python's `Scripts` directory, which is often not on PATH — prefer `pipx`, or add the directory manually.
|
|
103
|
+
3. Still stuck? Re-run the failing command with `FIXPM_DEBUG=1` exported — the hook then shows why its probe failed instead of swallowing errors.
|
|
104
|
+
|
|
105
|
+
## Contributing
|
|
106
|
+
|
|
107
|
+
Contributions welcome — especially **rule tables** for pnpm/yarn coverage.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/Max-code7997/fixpm && cd fixpm
|
|
111
|
+
pip install -e ".[dev]"
|
|
112
|
+
ruff check . && pytest
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Adding a rule
|
|
116
|
+
|
|
117
|
+
Rules are data, not code. To teach `fixpm` a new command, alias, flag, or common misspelling, edit the relevant table in `src/fixpm/rules/`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# src/fixpm/rules/npm.py
|
|
121
|
+
NPM = register(
|
|
122
|
+
ManagerSpec(
|
|
123
|
+
name="npm",
|
|
124
|
+
binaries=("npm",),
|
|
125
|
+
commands=("install", "run", "publish", ...), # canonical top-level commands
|
|
126
|
+
aliases={"i": "install", "innit": "init"}, # alt spelling -> canonical
|
|
127
|
+
flags={"install": ("--save-dev", "-D", ...)}, # validated flags per command
|
|
128
|
+
typo_hints={"isntall": "install"}, # deterministic overrides
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Guidelines:
|
|
134
|
+
|
|
135
|
+
- One PR per logical change (a new manager, a batch of flags, …).
|
|
136
|
+
- Add a pytest case in `tests/test_corrections.py` for every new behavior.
|
|
137
|
+
- Flag lists may be partial — unknown flags on unlisted commands are simply not validated (conservative by design).
|
|
138
|
+
|
|
139
|
+
See [CONTRIBUTING notes above](#contributing); open an issue first for new package managers.
|
|
140
|
+
|
|
141
|
+
## Roadmap
|
|
142
|
+
|
|
143
|
+
- [ ] Deeper pnpm / yarn (Berry) flag coverage
|
|
144
|
+
- [ ] fish & PowerShell hooks
|
|
145
|
+
- [ ] Validate `npm run <script>` against local `package.json`
|
|
146
|
+
- [ ] On-disk registry cache with TTL
|
|
147
|
+
- [ ] `fixpm --explain` (why this suggestion)
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fixpm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Interactive fixer for broken npm / npx / pnpm / yarn commands."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "fixpm contributors" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["npm", "npx", "pnpm", "yarn", "cli", "typo", "did-you-mean"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: POSIX",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"typer>=0.12",
|
|
30
|
+
"questionary>=2.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=8.0", "ruff>=0.4"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
fixpm = "fixpm.cli:app"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/fixpm"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py310"
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
49
|
+
ignore = ["E501", "B008"] # B008: typer uses function calls in defaults by design
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""CLI entry point.
|
|
2
|
+
|
|
3
|
+
Modes:
|
|
4
|
+
fixpm fix $FIXPM_LAST_COMMAND (set by shell hook)
|
|
5
|
+
fixpm <command words...> fix an explicit command line
|
|
6
|
+
fixpm --dry-run <cmd> print fixes without prompting/executing
|
|
7
|
+
fixpm --init zsh|bash emit the hook script for eval "$(...)"
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from importlib import resources
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .corrector import get_corrections
|
|
21
|
+
from .interactive import choose
|
|
22
|
+
from .rules import all_specs, spec_for_binary
|
|
23
|
+
from .rules.base import KIND_LABEL
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
add_completion=False,
|
|
27
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
28
|
+
help="Fix broken npm / npx / pnpm / yarn commands.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
ENV_COMMAND = "FIXPM_LAST_COMMAND"
|
|
32
|
+
ENV_EXIT_CODE = "FIXPM_LAST_EXIT_CODE"
|
|
33
|
+
|
|
34
|
+
_HOOKS = {"bash": "fixpm.bash", "zsh": "fixpm.zsh"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
if value:
|
|
39
|
+
typer.echo(f"fixpm {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback(invoke_without_command=True)
|
|
44
|
+
def main(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
command: list[str] | None = typer.Argument(
|
|
47
|
+
None, help="Failed command to fix. Reads $FIXPM_LAST_COMMAND when omitted."
|
|
48
|
+
),
|
|
49
|
+
manager: str | None = typer.Option(
|
|
50
|
+
None, "--manager", "-m",
|
|
51
|
+
help="Force a package manager instead of auto-detecting.",
|
|
52
|
+
),
|
|
53
|
+
dry_run: bool = typer.Option(
|
|
54
|
+
False, "--dry-run", help="Print fixes without prompting or executing."
|
|
55
|
+
),
|
|
56
|
+
version: bool = typer.Option(
|
|
57
|
+
False, "--version", "-V", callback=_version_callback, is_eager=True,
|
|
58
|
+
help="Show version and exit.",
|
|
59
|
+
),
|
|
60
|
+
init_shell: str | None = typer.Option(
|
|
61
|
+
None, "--init",
|
|
62
|
+
help='Emit a hook script for the given shell ("bash" or "zsh"). '
|
|
63
|
+
'Use inside eval: eval "$(fixpm --init zsh)".',
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
if init_shell is not None:
|
|
67
|
+
_emit_hook(init_shell)
|
|
68
|
+
return
|
|
69
|
+
if ctx.invoked_subcommand is not None:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
text = " ".join(command).strip() if command \
|
|
73
|
+
else os.environ.get(ENV_COMMAND, "").strip()
|
|
74
|
+
if not text:
|
|
75
|
+
typer.secho("No command provided.", fg="red")
|
|
76
|
+
typer.echo(
|
|
77
|
+
'Run `fixpm <failed command>` or install a shell hook first: '
|
|
78
|
+
'`fixpm --init zsh`.'
|
|
79
|
+
)
|
|
80
|
+
raise typer.Exit(code=1)
|
|
81
|
+
|
|
82
|
+
spec = None
|
|
83
|
+
if manager:
|
|
84
|
+
spec = spec_for_binary(manager)
|
|
85
|
+
if spec is None:
|
|
86
|
+
names = ", ".join(sorted({s.binaries[0] for s in all_specs()}))
|
|
87
|
+
raise typer.BadParameter(f"unknown manager {manager!r}; pick one of: {names}")
|
|
88
|
+
|
|
89
|
+
corrections = get_corrections(text, spec=spec)
|
|
90
|
+
if not corrections:
|
|
91
|
+
typer.secho(f"No fix found for: {text}", fg="yellow")
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
|
|
94
|
+
if dry_run:
|
|
95
|
+
for c in corrections:
|
|
96
|
+
typer.echo(f" [{KIND_LABEL[c.kind]}] {c.command}")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
choice = choose(corrections)
|
|
100
|
+
if choice is None:
|
|
101
|
+
typer.echo("Cancelled.")
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
typer.secho(f"-> {choice.command}", fg="cyan", bold=True)
|
|
105
|
+
if not choice.executable:
|
|
106
|
+
typer.echo("This fix needs manual arguments — copy it and fill them in.")
|
|
107
|
+
raise typer.Exit(code=0)
|
|
108
|
+
|
|
109
|
+
raise typer.Exit(code=subprocess.run(choice.command, shell=True).returncode)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _emit_hook(shell: str) -> None:
|
|
113
|
+
filename = _HOOKS.get(shell)
|
|
114
|
+
if filename is None:
|
|
115
|
+
supported = ", ".join(sorted(_HOOKS))
|
|
116
|
+
typer.secho(f"Unsupported shell {shell!r}. Supported: {supported}",
|
|
117
|
+
fg="red", err=True)
|
|
118
|
+
raise typer.Exit(code=1)
|
|
119
|
+
script = (resources.files("fixpm.shell") / filename).read_text(encoding="utf-8")
|
|
120
|
+
# Write LF-only bytes directly: Windows pipes translate "\n" to "\r\n",
|
|
121
|
+
# and a unix shell sourcing CRLF text ends up with broken hook definitions
|
|
122
|
+
# (functions named "...{\r"). Git Bash/MSYS silently tolerates CRLF, WSL
|
|
123
|
+
# and native Linux do not — so the raw byte stream must be LF.
|
|
124
|
+
sys.stdout.buffer.write(script.replace("\r\n", "\n").encode("utf-8"))
|
|
125
|
+
sys.stdout.buffer.flush()
|
|
126
|
+
rc_file = "~/.zshrc" if shell == "zsh" else "~/.bashrc"
|
|
127
|
+
typer.secho(f"# Add this line to your {rc_file}: "
|
|
128
|
+
f'eval "$(fixpm --init {shell})"', dim=True, err=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
app()
|