sequential-hooks 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.
- plugins/agy/_sequential_hooks/__init__.py +1 -0
- plugins/agy/_sequential_hooks/_adapter.py +566 -0
- plugins/agy/_sequential_hooks/_doctor.py +413 -0
- plugins/claude/_sequential_hooks/__init__.py +1 -0
- plugins/claude/_sequential_hooks/_adapter.py +560 -0
- plugins/claude/_sequential_hooks/_doctor.py +455 -0
- plugins/codex/_sequential_hooks/__init__.py +1 -0
- plugins/codex/_sequential_hooks/_adapter.py +466 -0
- plugins/codex/_sequential_hooks/_doctor.py +563 -0
- sequential_hooks/__init__.py +3 -0
- sequential_hooks/__main__.py +5 -0
- sequential_hooks/_arguments.py +222 -0
- sequential_hooks/_cleanup.py +88 -0
- sequential_hooks/_cli.py +440 -0
- sequential_hooks/_containment/__init__.py +410 -0
- sequential_hooks/_containment/_posix.py +236 -0
- sequential_hooks/_containment/_uncontained.py +239 -0
- sequential_hooks/_containment/_windows.py +853 -0
- sequential_hooks/_containment/_windows_api.py +570 -0
- sequential_hooks/_containment/_windows_launcher.py +189 -0
- sequential_hooks/_diagnostics.py +265 -0
- sequential_hooks/_doctor/__init__.py +342 -0
- sequential_hooks/_doctor/_command.py +469 -0
- sequential_hooks/_doctor/_common.py +448 -0
- sequential_hooks/_doctor/_types.py +94 -0
- sequential_hooks/_downstream.py +181 -0
- sequential_hooks/_executable.py +187 -0
- sequential_hooks/_executor.py +825 -0
- sequential_hooks/_registry.py +103 -0
- sequential_hooks/_runner.py +48 -0
- sequential_hooks/_types.py +125 -0
- sequential_hooks/hosts/__init__.py +130 -0
- sequential_hooks/hosts/_contract.py +434 -0
- sequential_hooks/hosts/_inspection.py +78 -0
- sequential_hooks/hosts/_json.py +150 -0
- sequential_hooks/hosts/_records.py +273 -0
- sequential_hooks/hosts/_skeleton.py +1326 -0
- sequential_hooks/py.typed +0 -0
- sequential_hooks-0.1.0.dist-info/METADATA +90 -0
- sequential_hooks-0.1.0.dist-info/RECORD +43 -0
- sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
- sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
- sequential_hooks-0.1.0.dist-info/licenses/LICENSE +21 -0
sequential_hooks/_cli.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
|
|
3
|
+
"""Implement command-line entry points for hooks and doctor checks."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path, PureWindowsPath
|
|
10
|
+
from typing import BinaryIO
|
|
11
|
+
|
|
12
|
+
from sequential_hooks._arguments import HookInvocation, InvocationError, parse_hook_argv
|
|
13
|
+
from sequential_hooks._cleanup import CleanupState
|
|
14
|
+
from sequential_hooks._diagnostics import Diagnostics
|
|
15
|
+
from sequential_hooks._downstream import closable_stdout, is_downstream_closure, silence_descriptor
|
|
16
|
+
from sequential_hooks._executor import ForwardedSignal, ProcessExecutor
|
|
17
|
+
from sequential_hooks._registry import HostLoadError, load_host
|
|
18
|
+
from sequential_hooks._runner import run_pipeline
|
|
19
|
+
from sequential_hooks.hosts import (
|
|
20
|
+
FinalResponse,
|
|
21
|
+
HostAdapter,
|
|
22
|
+
HostInputError,
|
|
23
|
+
HostSpec,
|
|
24
|
+
JsonObject,
|
|
25
|
+
)
|
|
26
|
+
from sequential_hooks.hosts._json import JsonProtocolError, load_object
|
|
27
|
+
|
|
28
|
+
_DOCTOR_HELP = """usage: sequential-hooks doctor --agent NAME
|
|
29
|
+
sequential-hooks doctor --help
|
|
30
|
+
|
|
31
|
+
Check coding-agent configuration and platform containment support.
|
|
32
|
+
|
|
33
|
+
options:
|
|
34
|
+
-a, --agent NAME registered coding agent
|
|
35
|
+
-h, --help
|
|
36
|
+
"""
|
|
37
|
+
_HELP = """usage: sequential-hooks --agent NAME [OPTIONS] CHILD [ARGS] [-- CHILD [ARGS]]...
|
|
38
|
+
sequential-hooks doctor --help
|
|
39
|
+
|
|
40
|
+
Compose one through three hook commands in order.
|
|
41
|
+
|
|
42
|
+
options:
|
|
43
|
+
-a, --agent NAME registered coding agent
|
|
44
|
+
-e, --event EVENT required when the agent payload carries no event
|
|
45
|
+
--step-timeout SECONDS
|
|
46
|
+
--allow-on-failure
|
|
47
|
+
-h, --help
|
|
48
|
+
--version
|
|
49
|
+
|
|
50
|
+
The exact -- token separates child commands and is unavailable to a child.
|
|
51
|
+
"""
|
|
52
|
+
_INTERNAL_FAILURE_DIAGNOSTIC = 'unexpected internal failure'
|
|
53
|
+
_INVALID_INPUT_DIAGNOSTIC = 'native hook input is invalid'
|
|
54
|
+
_INVALID_INVOCATION_DIAGNOSTIC = 'wrapper invocation is invalid'
|
|
55
|
+
_LAST_RESORT_EXIT = 2
|
|
56
|
+
_VALID_DOCTOR_ARGUMENT_COUNT = 3
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class _NativeContext:
|
|
61
|
+
"""Carry one parsed native input and its trusted event."""
|
|
62
|
+
|
|
63
|
+
_event: str
|
|
64
|
+
_raw_input: bytes
|
|
65
|
+
_payload: JsonObject
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _NoTrustedEventError(Exception):
|
|
69
|
+
"""Report that no event-specific native response can be produced."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _doctor_system_root(
|
|
73
|
+
cwd: Path,
|
|
74
|
+
*,
|
|
75
|
+
windows: bool,
|
|
76
|
+
environment: Mapping[str, str],
|
|
77
|
+
) -> Path:
|
|
78
|
+
"""Return the filesystem root used for machine-managed sources."""
|
|
79
|
+
if not windows:
|
|
80
|
+
return Path(cwd.anchor)
|
|
81
|
+
if system_drive := environment.get('SystemDrive'):
|
|
82
|
+
anchor = PureWindowsPath(f'{system_drive}\\').anchor
|
|
83
|
+
if anchor:
|
|
84
|
+
return Path(anchor)
|
|
85
|
+
for variable in ('SystemRoot', 'ProgramFiles'):
|
|
86
|
+
location = environment.get(variable)
|
|
87
|
+
if not location:
|
|
88
|
+
continue
|
|
89
|
+
anchor = PureWindowsPath(location).anchor
|
|
90
|
+
if anchor:
|
|
91
|
+
return Path(anchor)
|
|
92
|
+
return Path(cwd.anchor)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _explicit_event(spec: HostSpec, event: str | None) -> str:
|
|
96
|
+
"""Return the trusted `--event` for a host that requires one."""
|
|
97
|
+
if event is None:
|
|
98
|
+
raise _NoTrustedEventError(f'--event is required for host {spec.name}')
|
|
99
|
+
if event not in spec.events:
|
|
100
|
+
raise _NoTrustedEventError(f'host {spec.name} does not serve event {event!r}')
|
|
101
|
+
return event
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_text_command(arguments: tuple[str, ...]) -> bool:
|
|
105
|
+
"""Identify commands whose output is text rather than hook responses."""
|
|
106
|
+
return arguments in {('-h',), ('--help',), ('--version',)} or bool(
|
|
107
|
+
arguments and arguments[0] == 'doctor'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _last_resort(message: str, diagnostics: Diagnostics) -> int:
|
|
112
|
+
"""Answer with one diagnostic line, visible controls, and exit 2."""
|
|
113
|
+
diagnostics.write_global(message.encode('unicode_escape').decode('ascii'))
|
|
114
|
+
return _LAST_RESORT_EXIT
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _native_context(
|
|
118
|
+
spec: HostSpec,
|
|
119
|
+
event: str | None,
|
|
120
|
+
raw_input: bytes,
|
|
121
|
+
) -> _NativeContext:
|
|
122
|
+
"""Parse native input and bind the trusted event for the selected host."""
|
|
123
|
+
# Invalid JSON and unresolved host events remain distinct so callers can
|
|
124
|
+
# select the documented configuration or last-resort response.
|
|
125
|
+
if spec.requires_event:
|
|
126
|
+
trusted = _explicit_event(spec, event)
|
|
127
|
+
return _NativeContext(trusted, raw_input, load_object(raw_input))
|
|
128
|
+
payload = load_object(raw_input)
|
|
129
|
+
failure = CleanupState()
|
|
130
|
+
payload_event: str | None = None
|
|
131
|
+
with failure:
|
|
132
|
+
payload_event = spec.payload_event(payload)
|
|
133
|
+
if failure.first_error is not None:
|
|
134
|
+
raise _NoTrustedEventError(
|
|
135
|
+
f'host {spec.name} event resolution failed: {type(failure.first_error).__name__}'
|
|
136
|
+
) from failure.first_error
|
|
137
|
+
if payload_event is None or payload_event not in spec.events:
|
|
138
|
+
raise _NoTrustedEventError('native event could not be established')
|
|
139
|
+
return _NativeContext(payload_event, raw_input, payload)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _run_doctor_cli(
|
|
143
|
+
arguments: tuple[str, ...],
|
|
144
|
+
diagnostics: Diagnostics,
|
|
145
|
+
) -> int:
|
|
146
|
+
"""Parse and run the isolated doctor command grammar."""
|
|
147
|
+
if arguments in {('doctor', '-h'), ('doctor', '--help')}:
|
|
148
|
+
sys.stdout.write(_DOCTOR_HELP)
|
|
149
|
+
return 0
|
|
150
|
+
if (
|
|
151
|
+
len(arguments) != _VALID_DOCTOR_ARGUMENT_COUNT
|
|
152
|
+
or arguments[1] not in {'-a', '--agent'}
|
|
153
|
+
or not arguments[2]
|
|
154
|
+
or arguments[2].startswith('-')
|
|
155
|
+
):
|
|
156
|
+
diagnostics.write_global('invalid doctor invocation; use doctor --help')
|
|
157
|
+
return 2
|
|
158
|
+
host = arguments[2]
|
|
159
|
+
home = Path.home()
|
|
160
|
+
cwd = Path.cwd()
|
|
161
|
+
from sequential_hooks._doctor import run_doctor # noqa: PLC0415
|
|
162
|
+
|
|
163
|
+
return run_doctor(
|
|
164
|
+
host,
|
|
165
|
+
home=home,
|
|
166
|
+
cwd=cwd,
|
|
167
|
+
system_root=_doctor_system_root(
|
|
168
|
+
cwd,
|
|
169
|
+
windows=sys.platform == 'win32',
|
|
170
|
+
environment=os.environ,
|
|
171
|
+
),
|
|
172
|
+
path_value=os.environ.get('PATH'),
|
|
173
|
+
path_ext=os.environ.get('PATHEXT'),
|
|
174
|
+
stream=sys.stdout,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _trusted_event_for_error(
|
|
179
|
+
spec: HostSpec,
|
|
180
|
+
event: str | None,
|
|
181
|
+
stdin: BinaryIO,
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Establish the event a configuration response may answer."""
|
|
184
|
+
if spec.requires_event:
|
|
185
|
+
return _explicit_event(spec, event)
|
|
186
|
+
try:
|
|
187
|
+
return _native_context(spec, event, stdin.read())._event
|
|
188
|
+
except JsonProtocolError as error:
|
|
189
|
+
raise _NoTrustedEventError('native event could not be established') from error
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _version() -> str | None:
|
|
193
|
+
"""Return the installed version, or none when metadata is unavailable."""
|
|
194
|
+
import importlib.metadata # noqa: PLC0415
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
value = importlib.metadata.version('sequential-hooks')
|
|
198
|
+
except importlib.metadata.PackageNotFoundError:
|
|
199
|
+
return None
|
|
200
|
+
return f'sequential-hooks {value}\n'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _run_text_command(arguments: tuple[str, ...], diagnostics: Diagnostics) -> int:
|
|
204
|
+
"""Write top-level text or dispatch the doctor command."""
|
|
205
|
+
if arguments in {('-h',), ('--help',)}:
|
|
206
|
+
sys.stdout.write(_HELP)
|
|
207
|
+
return 0
|
|
208
|
+
if arguments == ('--version',):
|
|
209
|
+
version = _version()
|
|
210
|
+
if version is None:
|
|
211
|
+
diagnostics.write_global(
|
|
212
|
+
'installed distribution metadata for sequential-hooks is unavailable'
|
|
213
|
+
)
|
|
214
|
+
return 2
|
|
215
|
+
sys.stdout.write(version)
|
|
216
|
+
return 0
|
|
217
|
+
return _run_doctor_cli(arguments, diagnostics)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _write_response(stdout: BinaryIO, response: bytes) -> bool:
|
|
221
|
+
"""Write one response, returning false after a closed downstream pipe."""
|
|
222
|
+
try:
|
|
223
|
+
stdout.write(response)
|
|
224
|
+
stdout.flush()
|
|
225
|
+
except OSError as error:
|
|
226
|
+
if not is_downstream_closure(error):
|
|
227
|
+
raise
|
|
228
|
+
silence_descriptor(sys.stdout)
|
|
229
|
+
return False
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _configuration_response(
|
|
234
|
+
spec: HostSpec,
|
|
235
|
+
event: str,
|
|
236
|
+
message: str,
|
|
237
|
+
diagnostics: Diagnostics,
|
|
238
|
+
stdout: BinaryIO,
|
|
239
|
+
) -> int:
|
|
240
|
+
"""Resolve the host response before committing wrapper diagnostics."""
|
|
241
|
+
failure = CleanupState()
|
|
242
|
+
response: FinalResponse | None = None
|
|
243
|
+
response_exit_code = 0
|
|
244
|
+
response_stdout = b''
|
|
245
|
+
with failure:
|
|
246
|
+
response = spec.configuration_response(event, message, diagnostics)
|
|
247
|
+
if response is not None:
|
|
248
|
+
with failure:
|
|
249
|
+
response_stdout = response.stdout
|
|
250
|
+
response_exit_code = response.exit_code
|
|
251
|
+
if failure.first_error is not None:
|
|
252
|
+
return _last_resort(
|
|
253
|
+
f'{message}; host {spec.name} configuration response failed: '
|
|
254
|
+
f'{type(failure.first_error).__name__}',
|
|
255
|
+
diagnostics,
|
|
256
|
+
)
|
|
257
|
+
if response is None:
|
|
258
|
+
return _last_resort(
|
|
259
|
+
f'{message}; host {spec.name} returned no configuration response', diagnostics
|
|
260
|
+
)
|
|
261
|
+
diagnostics.write_global(message)
|
|
262
|
+
_write_response(stdout, response_stdout)
|
|
263
|
+
return response_exit_code
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _handle_parse_error(
|
|
267
|
+
error: InvocationError,
|
|
268
|
+
stdin: BinaryIO,
|
|
269
|
+
stdout: BinaryIO,
|
|
270
|
+
diagnostics: Diagnostics,
|
|
271
|
+
) -> int:
|
|
272
|
+
"""Translate syntax only after a host loads and its event is trusted."""
|
|
273
|
+
if error.host is None:
|
|
274
|
+
return _last_resort(str(error), diagnostics)
|
|
275
|
+
try:
|
|
276
|
+
spec = load_host(error.host)
|
|
277
|
+
except HostLoadError as load_error:
|
|
278
|
+
return _last_resort(f'{error}; {load_error}', diagnostics)
|
|
279
|
+
try:
|
|
280
|
+
event = _trusted_event_for_error(spec, error.event, stdin)
|
|
281
|
+
except _NoTrustedEventError as event_error:
|
|
282
|
+
return _last_resort(f'{error}; {event_error}', diagnostics)
|
|
283
|
+
return _configuration_response(
|
|
284
|
+
spec,
|
|
285
|
+
event,
|
|
286
|
+
_INVALID_INVOCATION_DIAGNOSTIC,
|
|
287
|
+
diagnostics,
|
|
288
|
+
stdout,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _prepare_adapter(
|
|
293
|
+
spec: HostSpec,
|
|
294
|
+
invocation: HookInvocation,
|
|
295
|
+
*,
|
|
296
|
+
stdin: BinaryIO,
|
|
297
|
+
stdout: BinaryIO,
|
|
298
|
+
diagnostics: Diagnostics,
|
|
299
|
+
) -> HostAdapter | int:
|
|
300
|
+
"""Read native input and construct the adapter, or answer a failure tier."""
|
|
301
|
+
raw_input = stdin.read()
|
|
302
|
+
try:
|
|
303
|
+
native_context = _native_context(spec, invocation.event, raw_input)
|
|
304
|
+
except _NoTrustedEventError as error:
|
|
305
|
+
return _last_resort(str(error), diagnostics)
|
|
306
|
+
except JsonProtocolError:
|
|
307
|
+
if invocation.event is None:
|
|
308
|
+
return _last_resort('native event could not be established', diagnostics)
|
|
309
|
+
return _configuration_response(
|
|
310
|
+
spec,
|
|
311
|
+
invocation.event,
|
|
312
|
+
_INVALID_INPUT_DIAGNOSTIC,
|
|
313
|
+
diagnostics,
|
|
314
|
+
stdout,
|
|
315
|
+
)
|
|
316
|
+
failure = CleanupState()
|
|
317
|
+
adapter: HostAdapter | None = None
|
|
318
|
+
with failure:
|
|
319
|
+
adapter = spec.adapter(
|
|
320
|
+
native_context._event,
|
|
321
|
+
native_context._raw_input,
|
|
322
|
+
native_context._payload,
|
|
323
|
+
diagnostics,
|
|
324
|
+
allow_on_failure=invocation.allow_on_failure,
|
|
325
|
+
)
|
|
326
|
+
if isinstance(failure.first_error, HostInputError):
|
|
327
|
+
return _configuration_response(
|
|
328
|
+
spec,
|
|
329
|
+
native_context._event,
|
|
330
|
+
f'{_INVALID_INPUT_DIAGNOSTIC}: {failure.first_error}',
|
|
331
|
+
diagnostics,
|
|
332
|
+
stdout,
|
|
333
|
+
)
|
|
334
|
+
if failure.first_error is not None or adapter is None:
|
|
335
|
+
return _last_resort(
|
|
336
|
+
f'host {spec.name} adapter failed: {type(failure.first_error).__name__}',
|
|
337
|
+
diagnostics,
|
|
338
|
+
)
|
|
339
|
+
return adapter
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _run_chain(
|
|
343
|
+
invocation: HookInvocation,
|
|
344
|
+
*,
|
|
345
|
+
adapter: HostAdapter,
|
|
346
|
+
stdout: BinaryIO,
|
|
347
|
+
diagnostics: Diagnostics,
|
|
348
|
+
) -> int:
|
|
349
|
+
"""Run the child chain and write the adapter's response."""
|
|
350
|
+
executor = ProcessExecutor(diagnostics)
|
|
351
|
+
failure = CleanupState()
|
|
352
|
+
response: FinalResponse | None = None
|
|
353
|
+
response_exit_code = 0
|
|
354
|
+
response_stdout = b''
|
|
355
|
+
try:
|
|
356
|
+
with executor.signal_forwarding(), failure:
|
|
357
|
+
response = run_pipeline(
|
|
358
|
+
adapter,
|
|
359
|
+
invocation.steps,
|
|
360
|
+
executor,
|
|
361
|
+
invocation.step_timeout,
|
|
362
|
+
allow_on_failure=invocation.allow_on_failure,
|
|
363
|
+
)
|
|
364
|
+
response_stdout = response.stdout
|
|
365
|
+
response_exit_code = response.exit_code
|
|
366
|
+
except ForwardedSignal as error:
|
|
367
|
+
return 128 + error.signum
|
|
368
|
+
if failure.first_error is not None or response is None:
|
|
369
|
+
return _last_resort(_INTERNAL_FAILURE_DIAGNOSTIC, diagnostics)
|
|
370
|
+
_write_response(stdout, response_stdout)
|
|
371
|
+
return response_exit_code
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _run_hook(
|
|
375
|
+
invocation: HookInvocation,
|
|
376
|
+
stdin: BinaryIO,
|
|
377
|
+
stdout: BinaryIO,
|
|
378
|
+
diagnostics: Diagnostics,
|
|
379
|
+
) -> int:
|
|
380
|
+
"""Run one parsed hook invocation through the three failure tiers."""
|
|
381
|
+
try:
|
|
382
|
+
spec = load_host(invocation.host)
|
|
383
|
+
except HostLoadError as error:
|
|
384
|
+
return _last_resort(str(error), diagnostics)
|
|
385
|
+
if not spec.requires_event and invocation.event is not None:
|
|
386
|
+
message = f'--event is not accepted for host {spec.name}'
|
|
387
|
+
try:
|
|
388
|
+
event = _trusted_event_for_error(spec, invocation.event, stdin)
|
|
389
|
+
except _NoTrustedEventError as error:
|
|
390
|
+
return _last_resort(f'{message}; {error}', diagnostics)
|
|
391
|
+
return _configuration_response(spec, event, message, diagnostics, stdout)
|
|
392
|
+
try:
|
|
393
|
+
if spec.requires_event:
|
|
394
|
+
_explicit_event(spec, invocation.event)
|
|
395
|
+
except _NoTrustedEventError as error:
|
|
396
|
+
return _last_resort(str(error), diagnostics)
|
|
397
|
+
prepared = _prepare_adapter(
|
|
398
|
+
spec,
|
|
399
|
+
invocation,
|
|
400
|
+
stdin=stdin,
|
|
401
|
+
stdout=stdout,
|
|
402
|
+
diagnostics=diagnostics,
|
|
403
|
+
)
|
|
404
|
+
if isinstance(prepared, int):
|
|
405
|
+
return prepared
|
|
406
|
+
return _run_chain(
|
|
407
|
+
invocation,
|
|
408
|
+
adapter=prepared,
|
|
409
|
+
stdout=stdout,
|
|
410
|
+
diagnostics=diagnostics,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
415
|
+
"""Run the command-line interface using the process standard streams.
|
|
416
|
+
|
|
417
|
+
Hook mode exchanges native bytes; help, version, and doctor write text to
|
|
418
|
+
stdout. Downstream closure is absorbed while preserving the command status.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
argv: Tokens after the executable name, or `None` for `sys.argv[1:]`.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Process exit status.
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
OSError: If writing, flushing, or redirecting output fails for a reason
|
|
428
|
+
other than downstream closure. A diagnostic `OSError` raised during
|
|
429
|
+
pipeline execution is translated under the internal-failure policy.
|
|
430
|
+
"""
|
|
431
|
+
arguments = tuple(sys.argv[1:] if argv is None else argv)
|
|
432
|
+
diagnostics = Diagnostics(sys.stderr)
|
|
433
|
+
if _is_text_command(arguments):
|
|
434
|
+
with closable_stdout():
|
|
435
|
+
return _run_text_command(arguments, diagnostics)
|
|
436
|
+
try:
|
|
437
|
+
invocation = parse_hook_argv(arguments)
|
|
438
|
+
except InvocationError as error:
|
|
439
|
+
return _handle_parse_error(error, sys.stdin.buffer, sys.stdout.buffer, diagnostics)
|
|
440
|
+
return _run_hook(invocation, sys.stdin.buffer, sys.stdout.buffer, diagnostics)
|