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.
@@ -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
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .idea/
13
+ .vscode/
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
+ ![demo](docs/assets/demo.gif) -->
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
+ ![demo](docs/assets/demo.gif) -->
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,3 @@
1
+ """fixpm — interactive command fixer for npm / npx / pnpm / yarn."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from fixpm.cli import app
2
+
3
+ app()
@@ -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()