errlens-cli 0.1.0__py3-none-any.whl

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.
errlens/runner.py ADDED
@@ -0,0 +1,241 @@
1
+ """Binary forwarding with independent readers and bounded process cleanup."""
2
+
3
+ import os
4
+ import errno
5
+ import queue
6
+ import select
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ from collections.abc import Callable, Iterator
13
+ from contextlib import contextmanager
14
+ from typing import BinaryIO, Literal
15
+ from types import FrameType
16
+
17
+ from errlens.capture import Capture
18
+ from errlens.models import ExecutionContext, ExecutionResult
19
+
20
+ CHUNK_SIZE = 64 * 1024
21
+
22
+
23
+ class RunnerError(Exception):
24
+ """ErrLens infrastructure failure, distinct from a child's nonzero status."""
25
+
26
+ def __init__(self, kind: str, detail: str, exit_code: int = 125) -> None:
27
+ super().__init__(detail)
28
+ self.kind = kind
29
+ self.exit_code = exit_code
30
+
31
+
32
+ def _broken_pipe(error: OSError | ValueError, destination: BinaryIO) -> bool:
33
+ if isinstance(error, BrokenPipeError):
34
+ return True
35
+ # Windows CRT can surface a closed redirected pipe as EINVAL, not EPIPE.
36
+ # Restrict the mapping to actual pipe handles; EINVAL on files stays an error.
37
+ if sys.platform == "win32" and isinstance(error, OSError) and error.errno == errno.EINVAL:
38
+ import ctypes
39
+ import msvcrt
40
+ from ctypes import wintypes
41
+
42
+ try:
43
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True)
44
+ get_type = kernel.GetFileType
45
+ get_type.argtypes = [wintypes.HANDLE]
46
+ get_type.restype = wintypes.DWORD
47
+ return bool(get_type(msvcrt.get_osfhandle(destination.fileno())) == 3)
48
+ except (OSError, ValueError):
49
+ return False
50
+ return False
51
+
52
+
53
+ def _available(stream: BinaryIO) -> int:
54
+ """Poll pipes without an uncancellable blocking read (also on Python 3.11)."""
55
+ if sys.platform != "win32":
56
+ return CHUNK_SIZE if select.select([stream], [], [], 0.02)[0] else 0
57
+ import ctypes
58
+ import msvcrt
59
+ from ctypes import wintypes
60
+
61
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True)
62
+ peek = kernel.PeekNamedPipe
63
+ peek.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
64
+ wintypes.LPVOID, ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
65
+ peek.restype = wintypes.BOOL
66
+ available = wintypes.DWORD()
67
+ if not peek(msvcrt.get_osfhandle(stream.fileno()), None, 0, None,
68
+ ctypes.byref(available), None):
69
+ error = ctypes.get_last_error()
70
+ if error in (109, 233): # broken pipe / no process on other end
71
+ return -1
72
+ raise ctypes.WinError(error)
73
+ return min(available.value, CHUNK_SIZE)
74
+
75
+
76
+ def _pump(
77
+ source: BinaryIO, destination: BinaryIO, name: Literal["stdout", "stderr"],
78
+ capture: Capture, stop: threading.Event, errors: queue.SimpleQueue[RunnerError],
79
+ ) -> None:
80
+ try:
81
+ while not stop.is_set():
82
+ try:
83
+ available = _available(source)
84
+ if available < 0:
85
+ return
86
+ if available == 0:
87
+ stop.wait(0.01)
88
+ continue
89
+ chunk = os.read(source.fileno(), available)
90
+ except (OSError, ValueError) as exc:
91
+ errors.put(RunnerError("read", f"{name}: {exc}"))
92
+ return
93
+ if not chunk:
94
+ return
95
+ capture.append(name, chunk)
96
+ try:
97
+ offset = 0
98
+ while offset < len(chunk):
99
+ written = destination.write(chunk[offset:])
100
+ if written is None or written <= 0:
101
+ raise OSError("Output stream made no progress")
102
+ offset += written
103
+ destination.flush()
104
+ except (OSError, ValueError) as exc:
105
+ broken = _broken_pipe(exc, destination)
106
+ errors.put(RunnerError("broken_pipe" if broken else "write", f"{name}: {exc}",
107
+ 141 if broken else 125))
108
+ return
109
+ except Exception as exc:
110
+ errors.put(RunnerError("reader", f"{name}: {type(exc).__name__}: {exc}"))
111
+ finally:
112
+ try:
113
+ source.close()
114
+ except OSError as exc:
115
+ errors.put(RunnerError("read", f"{name}: {exc}"))
116
+
117
+
118
+ @contextmanager
119
+ def _interrupts(cancel: threading.Event) -> Iterator[None]:
120
+ """Handlers record cancellation; no asynchronous exceptions during cleanup."""
121
+ previous: dict[signal.Signals, Callable[[int, FrameType | None], object] | int | None] = {}
122
+ if threading.current_thread() is threading.main_thread():
123
+ signals = [signal.SIGINT]
124
+ if sys.platform == "win32":
125
+ signals.append(signal.SIGBREAK)
126
+ for number in signals:
127
+ previous[number] = signal.getsignal(number)
128
+ if previous[number] != signal.SIG_IGN:
129
+ signal.signal(number, lambda signum, frame: cancel.set())
130
+ try:
131
+ yield
132
+ finally:
133
+ for number, handler in previous.items():
134
+ signal.signal(number, handler)
135
+
136
+
137
+ def _signal_group(process: subprocess.Popen[bytes], *, force: bool = False) -> None:
138
+ try:
139
+ if sys.platform != "win32":
140
+ os.killpg(process.pid, signal.SIGKILL if force else signal.SIGINT)
141
+ elif process.poll() is None:
142
+ if force:
143
+ process.kill()
144
+ else:
145
+ process.send_signal(signal.CTRL_BREAK_EVENT)
146
+ except ProcessLookupError:
147
+ pass
148
+ except OSError:
149
+ if process.poll() is None:
150
+ process.kill()
151
+
152
+
153
+ def run(
154
+ context: ExecutionContext, capture: Capture, stdout: BinaryIO, stderr: BinaryIO,
155
+ *, cancel: threading.Event | None = None, interrupt_timeout: float = 1.0,
156
+ drain_timeout: float = 1.0,
157
+ ) -> ExecutionResult:
158
+ """Run an argv sequence, inheriting stdin and environment (metadata is inert).
159
+
160
+ Single-use capture is supplied by the caller. Main-thread use handles Ctrl+C;
161
+ callers in other threads must supply a cancellation Event. Exit signals remain
162
+ negative in the result. No analyzers, rendering or translations are invoked.
163
+ """
164
+ if interrupt_timeout <= 0 or drain_timeout <= 0:
165
+ raise ValueError("Timeouts must be positive")
166
+ cancellation = cancel if cancel is not None else threading.Event()
167
+ errors: queue.SimpleQueue[RunnerError] = queue.SimpleQueue()
168
+ stop = threading.Event()
169
+ readers: list[threading.Thread] = []
170
+ process: subprocess.Popen[bytes] | None = None
171
+ with _interrupts(cancellation):
172
+ try:
173
+ try:
174
+ creation_flags = 0
175
+ if sys.platform == "win32":
176
+ creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP
177
+ process = subprocess.Popen(
178
+ context.command, cwd=context.cwd, stdout=subprocess.PIPE,
179
+ stderr=subprocess.PIPE, bufsize=0, shell=False,
180
+ start_new_session=os.name == "posix",
181
+ creationflags=creation_flags,
182
+ )
183
+ except FileNotFoundError as exc:
184
+ raise RunnerError("not_found", str(exc), 127) from exc
185
+ except PermissionError as exc:
186
+ raise RunnerError("permission", str(exc), 126) from exc
187
+ except (OSError, ValueError) as exc:
188
+ raise RunnerError("launch", str(exc)) from exc
189
+ assert process.stdout is not None and process.stderr is not None
190
+ for name, source, target in (
191
+ ("stdout", process.stdout, stdout), ("stderr", process.stderr, stderr),
192
+ ):
193
+ reader = threading.Thread(
194
+ target=_pump, args=(source, target, name, capture, stop, errors),
195
+ name=f"errlens-{name}", daemon=True,
196
+ )
197
+ reader.start()
198
+ readers.append(reader)
199
+ interrupted = False
200
+ interrupt_deadline: float | None = None
201
+ drain_deadline: float | None = None
202
+ while True:
203
+ if not errors.empty():
204
+ raise errors.get()
205
+ now = time.monotonic()
206
+ if cancellation.is_set() and not interrupted:
207
+ interrupted = True
208
+ _signal_group(process)
209
+ interrupt_deadline = now + interrupt_timeout
210
+ if interrupt_deadline is not None and now >= interrupt_deadline:
211
+ _signal_group(process, force=True)
212
+ interrupt_deadline = None
213
+ status = process.poll()
214
+ if status is not None:
215
+ if not any(reader.is_alive() for reader in readers):
216
+ break
217
+ if drain_deadline is None:
218
+ drain_deadline = now + drain_timeout
219
+ if now >= drain_deadline:
220
+ raise RunnerError("drain", "Output pipes did not close after process exit")
221
+ time.sleep(0.01)
222
+ if not errors.empty():
223
+ raise errors.get()
224
+ return ExecutionResult(status, interrupted, not capture.truncated)
225
+ finally:
226
+ stop.set()
227
+ if process is not None:
228
+ if process.poll() is None or cancellation.is_set() or any(r.is_alive() for r in readers):
229
+ _signal_group(process, force=True)
230
+ try:
231
+ process.wait(timeout=2)
232
+ except subprocess.TimeoutExpired as exc:
233
+ raise RunnerError("cleanup", "Process did not exit after kill") from exc
234
+ for reader in readers:
235
+ reader.join(timeout=0.5)
236
+ # Readers own their source handles; never close/reuse an fd while
237
+ # a reader is active. Normally polling makes both joins immediate.
238
+ if not any(reader.is_alive() for reader in readers):
239
+ for pipe in (process.stdout, process.stderr):
240
+ if pipe is not None and not pipe.closed:
241
+ pipe.close()
errlens/service.py ADDED
@@ -0,0 +1,58 @@
1
+ """Post-execution analysis/explanation coordination, without process or terminal I/O."""
2
+
3
+ from errlens.analyzers.base import AnalyzerRegistry
4
+ from errlens.analyzers.python import PythonAnalyzer
5
+ from errlens.capture import Capture, TerminalCapture
6
+ from errlens.models import Diagnostic, ExecutionContext, ExecutionResult, ExplanationReport
7
+ from errlens.rules.python import explain
8
+
9
+
10
+ def analyze_capture(
11
+ context: ExecutionContext, result: ExecutionResult, capture: Capture | TerminalCapture,
12
+ *, registry: AnalyzerRegistry | None = None, encoding: str = "utf-8",
13
+ ) -> list[Diagnostic]:
14
+ """Analyze bounded tails once the runner has finished. Inputs are not modified.
15
+
16
+ Callers can inspect structured results without reparsing the original bytes.
17
+ """
18
+ if registry is None:
19
+ registry = AnalyzerRegistry()
20
+ registry.register(PythonAnalyzer())
21
+ data = capture.decoded(encoding)
22
+ diagnostics: list[Diagnostic] = []
23
+ for analyzer in registry.all():
24
+ diagnostics.extend(analyzer.analyze(context, result, data))
25
+ return diagnostics
26
+
27
+
28
+ def explain_capture(
29
+ context: ExecutionContext, result: ExecutionResult, capture: Capture | TerminalCapture,
30
+ *, registry: AnalyzerRegistry | None = None, encoding: str = "utf-8",
31
+ ) -> ExplanationReport | None:
32
+ """One report after a failed, non-interrupted execution; no output on success.
33
+
34
+ Prefer stderr, then combined, then stdout. Follow explicit chain links only;
35
+ independent diagnostics are counted, never labelled as causes.
36
+ """
37
+ if result.exit_code in (0, None) or result.interrupted:
38
+ return None
39
+ diagnostics = analyze_capture(context, result, capture, registry=registry, encoding=encoding)
40
+ if not diagnostics:
41
+ return None
42
+ primary = next((items[-1] for source in ("stderr", "combined", "stdout")
43
+ if (items := [item for item in diagnostics if item.source == source])),
44
+ diagnostics[-1])
45
+ indexed = {item.diagnostic_id: item for item in diagnostics}
46
+ related: list[Diagnostic] = []
47
+ seen = {primary.diagnostic_id}
48
+ current = primary
49
+ while current.related_diagnostic_id and current.related_diagnostic_id not in seen:
50
+ predecessor = indexed.get(current.related_diagnostic_id)
51
+ if predecessor is None:
52
+ break
53
+ related.append(predecessor)
54
+ seen.add(predecessor.diagnostic_id)
55
+ current = predecessor
56
+ related.reverse()
57
+ return ExplanationReport(primary, explain(primary, context), related,
58
+ max(0, len(diagnostics) - len(related) - 1))
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: errlens-cli
3
+ Version: 0.1.0
4
+ Summary: Local Python error explanations with opt-in terminal and shell integration.
5
+ Author: ErrLens contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ErrLens/errlens-cli
8
+ Project-URL: Source, https://github.com/ErrLens/errlens-cli
9
+ Project-URL: Documentation, https://github.com/ErrLens/errlens-cli/blob/main/README.md
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: typer<1,>=0.16
19
+ Requires-Dist: rich<15,>=14
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest<10,>=8; extra == "dev"
22
+ Requires-Dist: build<2,>=1.2; extra == "dev"
23
+ Requires-Dist: mypy<2,>=1.15; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # ErrLens
27
+
28
+ ErrLens is a local, open-source CLI that adds readable Russian or English explanations to recognized Python errors. It helps developers understand tracebacks without copying them to an external service. Explanations are rule-based and work offline after installation; no API key or AI model is required.
29
+
30
+ ErrLens runs a command through `errlens run`. Its default pipe mode forwards the original stdout and stderr, then appends an explanation to stderr when an error is recognized. Optional shell wrappers intercept only `python`; explicit terminal mode uses merged terminal output.
31
+
32
+ ## Requirements and installation
33
+
34
+ Python 3.11 or newer is required. On Windows, Linux or macOS, install from a local checkout:
35
+
36
+ ```console
37
+ python -m pip install .
38
+ ```
39
+
40
+ For development, use a virtual environment and install `python -m pip install -e ".[dev]"`. Installing dependencies requires an available package index or local wheels.
41
+
42
+ The PyPI distribution name is **errlens-cli**; the Python import and executable
43
+ are both **errlens**. This candidate has not been uploaded. After publication,
44
+ install with `python -m pip install errlens-cli==0.1.0`, then run `errlens --version`.
45
+ Until then use this checkout or a verified supplied wheel:
46
+ `python -m pip install /path/to/errlens_cli-0.1.0-py3-none-any.whl`.
47
+ The existing PyPI package named `errlens` is not the distribution selected for
48
+ this repository. See [release preparation](https://github.com/ErrLens/errlens-cli/blob/main/docs/releasing.md).
49
+ CI covers CPython 3.11–3.13; newer Python versions are not yet qualified.
50
+
51
+ ## Usage
52
+
53
+ ```console
54
+ errlens --help
55
+ errlens --version
56
+ errlens run python main.py
57
+ errlens run --lang en -- python main.py
58
+ errlens run --no-explain -- python main.py
59
+ errlens run python -c "print(undefined_variable)"
60
+ ```
61
+
62
+ Russian is the default; `--lang en` selects English. Place ErrLens options before the executable; arguments after the executable belong to the child process. `--no-explain` skips analysis. The optional `--` makes the boundary explicit.
63
+
64
+ The original traceback remains visible. For example, a `NameError` can be followed by an explanation that the name is unavailable in the current scope and suggestions to check its spelling and definition. Causes are described as possibilities, not established facts. Successful commands and nonzero exits without recognized diagnostics do not produce an explanation.
65
+
66
+ ## Supported diagnostics
67
+
68
+ Rules cover `ModuleNotFoundError`, `ImportError`, `SyntaxError`, `IndentationError`, `TabError`, `NameError`, `TypeError`, `AttributeError`, `KeyError`, `FileNotFoundError` and `PermissionError`. Other structured exceptions receive a neutral notice. Incomplete diagnostics are marked as such; `ExceptionGroup` is summarized at its outer level. For a chain of exceptions, the final diagnostic is explained and linked predecessors are noted.
69
+
70
+ Suggested commands are never executed. A pip command is shown only for the exact `requests` import and an explicitly specified absolute Python executable path, with a condition to confirm that the distribution is actually needed in that environment. Import names do not generally identify package names, and a missing module may be local.
71
+
72
+ ## Behavior and limitations
73
+
74
+ - ErrLens launches argument sequences without `shell=True`. Shell pipelines and builtins are interpreted by the invoking shell, not by ErrLens. On Windows, the operating system may invoke a command interpreter for `.bat` or `.cmd` files even with `shell=False`.
75
+ - Separate bounded buffers retain up to 1 MiB per stream for analysis. Original bytes are forwarded as they arrive; the diagnostic copy may be truncated. No output logs are saved to disk.
76
+ - Pipes change `isatty()` and may affect child buffering and colors. Interactive programs, job control and full-screen applications are not guaranteed to work. The relative ordering of stdout and stderr cannot be reconstructed.
77
+ - The child exit code is preserved for normal exits. On POSIX, signal termination becomes `128 + signal`; user interruption returns 130. Runner failures have their own codes: 125 for internal failures, 126 for permission errors, 127 for missing executables and 141 for broken pipes. Child programs can also use these codes; ErrLens identifies its own failures in stderr when possible.
78
+ - In pipe mode, Ctrl+C handling uses a separate process group. Windows pipe cleanup does not guarantee termination of an arbitrary descendant tree. After the child exits, pipes held open by descendants are subject to a one-second drain timeout. ConPTY uses the Job Object cleanup described below.
79
+ - Recognition targets standard CPython traceback formatting; IPython/Jupyter and logger-prefixed tracebacks are not supported. A traceback-like log can be ambiguous. Stdout diagnostics require a failed Python command. Unknown or incomplete data is not presented as a certain cause.
80
+ - Rich markup and terminal control characters from diagnostic data are rendered literally or escaped in the ErrLens block. Redirected explanations are plain text without ANSI. Original child output is not sanitized.
81
+ - Explanation fields are capped at 2,048 characters; linked exception messages at 256 characters. The forwarded traceback is not shortened by this limit.
82
+ - ErrLens does not fix code, install packages, collect telemetry or contact a service. Shell wrappers and terminal mode are explicit opt-ins; VS Code extension support remains future work.
83
+
84
+ ## Opt-in PowerShell 7 integration (Windows)
85
+
86
+ ```powershell
87
+ errlens shell install powershell
88
+ errlens shell status powershell
89
+ errlens shell uninstall powershell
90
+ ```
91
+
92
+ Installation explicitly edits the current user's **all-hosts PowerShell 7 profile**
93
+ (shared with VS Code), discovered using `pwsh -NoProfile`. Restart PowerShell after
94
+ installation/removal. ExecutionPolicy is never changed; your policy must allow
95
+ loading the profile and the integration script. No administrator access is needed.
96
+ For an isolated trial or a custom host profile use `--profile 'path/to/profile.ps1'`
97
+ on each management command, then dot-source that profile manually.
98
+
99
+ An owned script is placed in an `ErrLens` subdirectory next to the selected profile.
100
+ Only one guarded dot-source line is appended. Existing profile bytes are preserved;
101
+ each modification of an existing profile creates a uniquely named backup alongside
102
+ it. Backups are retained on removal. Edited connection lines and unowned integration
103
+ files are refused rather than overwritten. A newly created profile is left empty
104
+ after removal. Existing `python` aliases/functions are preserved with a warning.
105
+
106
+ After loading, plain `python main.py`, `python -c "print(undefined_variable)"` and
107
+ `python --version` delegate to `errlens run`. Only the command name `python` is
108
+ wrapped. Absolute executable paths, `py`, pip and other commands bypass it.
109
+ The native Python application is resolved from PATH on every invocation, so venv
110
+ activation is respected. The ErrLens runtime is bound to the absolute Python path
111
+ used during installation; `errlens` need not be in PATH. Keep that environment
112
+ installed. To migrate it, uninstall the integration and reinstall from the new runtime.
113
+
114
+ Temporarily bypass analysis with `$env:ERRLENS_DISABLE='1'`; restore it with
115
+ `Remove-Item Env:ERRLENS_DISABLE`. To remove the wrapper from the current session,
116
+ use `Remove-Item Function:python`. Persistent removal takes effect in new sessions.
117
+
118
+ This is a PowerShell function calling a native program, not transparent terminal
119
+ capture. `$LASTEXITCODE` is explicitly propagated. `$?` may report function success
120
+ even when Python fails; inspect `$LASTEXITCODE`. PowerShell 7.3+ uses Standard
121
+ native argument passing locally inside the wrapper; 7.0–7.2 retain their legacy
122
+ quoting/empty-argument limitations. Global preferences are not changed.
123
+ Pipeline output and `>` pass through PowerShell's object/text conversion and do
124
+ not guarantee byte identity (including PowerShell 7.4 native redirection behavior).
125
+ Pipeline input is not forwarded by this wrapper. Avoid binary pipelines, REPLs,
126
+ interactive and full-screen applications; use an absolute Python executable when
127
+ native semantics are needed. ExecutionPolicy, host-specific profile loading and
128
+ VS Code itself require manual verification on the target machine.
129
+
130
+ ## Opt-in Bash and Zsh integration
131
+
132
+ On Linux/macOS, explicitly install with `errlens shell install bash` or
133
+ `errlens shell install zsh`; use `status` or `uninstall` in place of `install`.
134
+ Bash uses `~/.bashrc`; Zsh uses `${ZDOTDIR:-$HOME}/.zshrc`. Bash login shells
135
+ do not necessarily load `.bashrc`: configure your login profile to source it
136
+ yourself if needed. These are interactive startup files; noninteractive shells
137
+ do not automatically load them. `--profile PATH` selects an isolated/custom file.
138
+
139
+ An owned script in `.errlens` beside the profile is sourced by one guarded line.
140
+ Existing profile bytes are retained, changes receive unique backups, repeated
141
+ installation is idempotent, and removal removes only the exact owned line and script.
142
+ Edited lines and unowned scripts are refused. Backups remain after removal.
143
+ Existing `python` functions and aliases take precedence and are preserved.
144
+
145
+ The `python` function resolves the external interpreter and `errlens` on each
146
+ invocation, forwards `"$@"`, working directory and exit status, and respects an
147
+ activated virtual environment. `ERRLENS_DISABLE=1 python ...` bypasses analysis;
148
+ if ErrLens is absent from PATH, the native Python is run directly. Use `unset -f
149
+ python` to disable the wrapper in the current shell. `command python`, an absolute
150
+ path, `python3`, pip and other commands bypass it. Restart or source the profile
151
+ after installation; restart or unset the function after uninstallation.
152
+
153
+ When starting Zsh from Bash with an active virtual environment, the Bash
154
+ activation script may have modified and exported prompt variables. Bash prompt
155
+ escapes are not Zsh prompt syntax and may appear literally. Start a clean Zsh,
156
+ then run `source /path/to/venv/bin/activate` within Zsh. The ErrLens integration
157
+ does not change `PS1` or `PROMPT`; your Zsh configuration can define its own prompt.
158
+ Manual reports of Windows 11/PowerShell and Ubuntu 24.04/WSL Bash and Zsh are
159
+ distinct from the automated CI checks; consult CI results for verified runs.
160
+
161
+ Redirection and pipelines follow the shell and `errlens run` pipe semantics.
162
+ Input is inherited, but interactive/terminal-dependent programs and exact stdout/
163
+ stderr interleaving are not guaranteed. Ctrl+C follows Runner process-group handling;
164
+ PTY and job-control equivalence are not guaranteed. Paths with spaces and Unicode
165
+ are quoted without `eval`. No system profiles or shell settings are modified.
166
+
167
+ ## Explicit terminal mode
168
+
169
+ Use `errlens run --terminal-mode pty -- python script.py` when terminal stdin
170
+ and stdout are available. Linux/macOS use POSIX PTY; Windows 10 1809+ and Windows
171
+ 11 use native ConPTY. Pipe remains the default, including all shell wrappers.
172
+ A transport error never triggers an automatic second run. Windows requires
173
+ actual console handles and a native executable (.exe or .com), with no batch
174
+ file or legacy winpty fallback. No extra runtime dependency or compiler is needed.
175
+
176
+ Terminal mode supplies a terminal, input, window-size propagation and terminal
177
+ Ctrl+C handling. stdout and stderr merge onto ErrLens stdout; echo and LF-to-CRLF
178
+ conversion are normal terminal behavior. Binary identity and original stream
179
+ separation are not guaranteed. Redirected host stdin/stdout are rejected; use
180
+ pipe mode for pipelines. The combined diagnostic tail is limited to 1 MiB.
181
+ Recognized failed Python commands receive one explanation on stderr after terminal
182
+ restoration. Interactive REPL exceptions followed by a successful exit do not
183
+ produce cards. ANSI forwarding is supported, but screen reconstruction, job-control
184
+ transparency, suspended children and arbitrary full-screen applications are not
185
+ guaranteed. A permanently blocked output sink can still block forwarding.
186
+
187
+ The host terminal modes and signal handlers are saved and restored on ordinary
188
+ exit, cancellation and transport errors. SIGKILL or a host crash cannot provide
189
+ that guarantee. POSIX processes that deliberately detach from the session may
190
+ outlive cleanup. ConPTY clients belong to a kill-on-close Windows Job Object:
191
+ remaining descendants are terminated when the main client ends or transport fails.
192
+ Windows uses a separate, short-lived owner process to bound ConPTY shutdown on
193
+ older Windows builds. Input/output are UTF-8/VT text, so invalid bytes, NUL, cursor
194
+ movement, wrapping and screen redraws need not survive as original log bytes.
195
+ The analyzer strips ANSI but does not reconstruct a screen; complex redraws can
196
+ produce incomplete or unrecognized diagnostics.
197
+
198
+ Automated tests use isolated real ConPTY sessions and nested console handles on
199
+ Windows, and nested POSIX PTYs on Linux and in the manual macOS workflow. User-reported manual checks
200
+ cover Windows Terminal and VS Code ConPTY plus selected WSL Bash/Zsh sessions;
201
+ the [acceptance record](https://github.com/ErrLens/errlens-cli/blob/main/docs/manual-acceptance.md) preserves untested combinations.
202
+ Legacy console hosts, detailed REPL editing, Windows 10 lifecycle behavior and
203
+ physical macOS hosts are not fully manually qualified. See
204
+ [Windows ConPTY notes](https://github.com/ErrLens/errlens-cli/blob/main/docs/windows-conpty.md) for lifecycle details.
205
+
206
+ ## Development and checks
207
+
208
+ ```console
209
+ python -m pytest
210
+ python -m mypy
211
+ python -m pip check
212
+ python -m build
213
+ ```
214
+
215
+ The build creates a wheel and source distribution in `dist/`. GitHub Actions #6
216
+ passed all 11 jobs for `84dbb9c927b1b0eaf3ac6efc50863b9f0ef72836`, covering
217
+ Windows, Ubuntu and macOS on Python 3.11–3.13 plus Windows/Ubuntu wheel installation.
218
+ Hosted macOS runners were unavailable in [CI #8](https://github.com/ErrLens/errlens-cli/actions/runs/43235376928).
219
+ The required Tests workflow temporarily runs Linux and Windows on Python 3.11–3.13
220
+ and both packaging jobs. macOS tests remain available through a separate manual
221
+ workflow. Earlier macOS passes apply to their tested commits. The release candidate
222
+ has yet to be tested on macOS. The [release guide](https://github.com/ErrLens/errlens-cli/blob/main/docs/releasing.md)
223
+ records when to restore macOS to the required matrix. The manual publishing workflow
224
+ can retain candidate artifacts for review.
225
+
226
+ See [CONTRIBUTING.md](https://github.com/ErrLens/errlens-cli/blob/main/CONTRIBUTING.md) for contribution guidance. ErrLens is distributed under the [MIT License](https://github.com/ErrLens/errlens-cli/blob/main/LICENSE).
227
+
228
+ ## Roadmap
229
+
230
+ The current CLI focuses on local Python diagnostics, localized explanations,
231
+ opt-in shell wrappers and explicit terminal sessions. Future stages may improve
232
+ terminal compatibility and add VS Code integration, additional analyzers and
233
+ optional AI providers. Those future features are not part of the current CLI.
@@ -0,0 +1,30 @@
1
+ errlens/__init__.py,sha256=AkRndmVGIVs_sUxxABG7XRcPS3OPBa_r-ErjfPWqmj8,186
2
+ errlens/__main__.py,sha256=I_fRhsEjjNCRvt75dfF4Bik486DYs49IWxi-_S2D7u4,118
3
+ errlens/_conpty.py,sha256=zLMY7uoKLGtNrbixeRcei-bs6TQaF30-FVuA2IDcuY8,9983
4
+ errlens/_conpty_worker.py,sha256=IOhUg9ycO3drOqLH97hk_vjn1f63iD30njycCtScwKM,6020
5
+ errlens/_pty_child.py,sha256=S_4Sl7bAZuo0Chj0RTE0exj2LSYLJdzLw1h0u3Qop08,719
6
+ errlens/capture.py,sha256=xZZlP_LhXqUDmkV_YAUFZKHDvHUEXzYu_x8pVIUZMfA,2994
7
+ errlens/cli.py,sha256=y4P2Vka0h9i_43kxt-jogRRSGwZET9vZVjKGev05ZoM,6954
8
+ errlens/conpty_runner.py,sha256=ABp8xGH-4UGVBSDuGTKz30A30fqIALpJKcq2LkEq1XQ,12408
9
+ errlens/i18n.py,sha256=reY8VjIdUL5kC-S-GdiIa997wrVafmTS7LnoOao4ffM,2390
10
+ errlens/models.py,sha256=Enjn-C4FwJ1BzFIrki5wFOOMAXFo-NJO_YR6UFtn_Cw,4060
11
+ errlens/posix_shell.py,sha256=3i4AV3SjaDoM8TIVS9_YOS9WX9ub_-wtVn_qGW78iq8,3074
12
+ errlens/powershell.py,sha256=nPlGZDWJ_EREGMmBj85LqwzqKkWHvxTJT6t5CMBDR5s,3698
13
+ errlens/pty_runner.py,sha256=33oskB5IlpB3PXAGAVk32m270VPccYvRNRIKFqASnW0,7084
14
+ errlens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ errlens/render.py,sha256=1PX78Vtau3BkxEzoaR_tRVx6XUfxdPEaS-oa-YMmeHo,4696
16
+ errlens/runner.py,sha256=tZ1zGFf9-ipOP2TnRa4YbKvI_OKYmD0837HCk4mBn9A,10020
17
+ errlens/service.py,sha256=WpKo9R9OiTlI4ggFID2_nBfirRdSkgi9apw_2ETkpTA,2602
18
+ errlens/analyzers/__init__.py,sha256=Lf3L5CqPi18geW4Fe7gdsvDeQHDHz--j1cdLFDhLaSA,78
19
+ errlens/analyzers/base.py,sha256=04kj74wNdB-0tZ0pjky--o2kN8Q6cD25TqamrW4_URE,1424
20
+ errlens/analyzers/python.py,sha256=HYcfe9h1wqrNcUHbK7f-BHJJeKCxfyhTF9-Cj3drdDA,16244
21
+ errlens/locales/en.json,sha256=AsFTM4rThOqyqPipZtlIq-qnZhrSTT1VQLnMUn5ZPTc,9107
22
+ errlens/locales/ru.json,sha256=XFJW_d_8nzJrTJsG65xqq-s7j8W5tXr2MSAkDAgyEHI,14288
23
+ errlens/rules/__init__.py,sha256=imlafOfMafltS594ewopLR1cd_0O348cJFpkaQ4LoyM,74
24
+ errlens/rules/python.py,sha256=fmxGawpIoPYOT3JVCf9p_j8AeqKZkkmsKrX6tBnnNpo,3681
25
+ errlens_cli-0.1.0.dist-info/licenses/LICENSE,sha256=w-U6mi6O-gzqwvlP9IB3m6Zr81MW1wNkqa3I_VBEPfs,1077
26
+ errlens_cli-0.1.0.dist-info/METADATA,sha256=VGQgNhOuH3Go0y6vJ_r2lbdHK7ulPEAoAB-LHnBZ_1Q,15870
27
+ errlens_cli-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
28
+ errlens_cli-0.1.0.dist-info/entry_points.txt,sha256=huj9rHpK07NvEqo5_Mluz5IvcEL3aENtZCr2e-gHj8k,51
29
+ errlens_cli-0.1.0.dist-info/top_level.txt,sha256=MlJH5FSVsEJQu1-spNcKDl9aXfrLT2B1ANsP6xBgxzI,8
30
+ errlens_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ errlens = errlens.cli:main_entry
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ErrLens 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.
@@ -0,0 +1 @@
1
+ errlens