ostruct-cli 0.3.0__py3-none-any.whl → 0.5.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.
- ostruct/cli/base_errors.py +183 -0
- ostruct/cli/cli.py +830 -585
- ostruct/cli/click_options.py +338 -211
- ostruct/cli/errors.py +214 -227
- ostruct/cli/exit_codes.py +18 -0
- ostruct/cli/file_info.py +126 -69
- ostruct/cli/file_list.py +191 -72
- ostruct/cli/file_utils.py +132 -97
- ostruct/cli/path_utils.py +86 -77
- ostruct/cli/security/__init__.py +32 -0
- ostruct/cli/security/allowed_checker.py +55 -0
- ostruct/cli/security/base.py +46 -0
- ostruct/cli/security/case_manager.py +75 -0
- ostruct/cli/security/errors.py +164 -0
- ostruct/cli/security/normalization.py +161 -0
- ostruct/cli/security/safe_joiner.py +211 -0
- ostruct/cli/security/security_manager.py +366 -0
- ostruct/cli/security/symlink_resolver.py +483 -0
- ostruct/cli/security/types.py +108 -0
- ostruct/cli/security/windows_paths.py +404 -0
- ostruct/cli/serialization.py +25 -0
- ostruct/cli/template_filters.py +13 -8
- ostruct/cli/template_rendering.py +46 -22
- ostruct/cli/template_utils.py +12 -4
- ostruct/cli/template_validation.py +26 -8
- ostruct/cli/token_utils.py +43 -0
- ostruct/cli/validators.py +109 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/METADATA +64 -24
- ostruct_cli-0.5.0.dist-info/RECORD +42 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/WHEEL +1 -1
- ostruct/cli/security.py +0 -964
- ostruct/cli/security_types.py +0 -46
- ostruct_cli-0.3.0.dist-info/RECORD +0 -28
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/click_options.py
CHANGED
@@ -1,248 +1,375 @@
|
|
1
1
|
"""Click command and options for the CLI.
|
2
2
|
|
3
3
|
This module contains all Click-related code separated from the main CLI logic.
|
4
|
-
We isolate this code here and
|
5
|
-
|
6
|
-
many type: ignore comments in the main code.
|
4
|
+
We isolate this code here and provide proper type annotations for Click's
|
5
|
+
decorator-based API.
|
7
6
|
"""
|
8
7
|
|
9
|
-
|
10
|
-
# ^ This tells mypy to ignore type checking for this entire file
|
11
|
-
|
12
|
-
from typing import Any, Callable
|
8
|
+
from typing import Any, Callable, TypeVar, Union, cast
|
13
9
|
|
14
10
|
import click
|
11
|
+
from click import Command
|
12
|
+
from typing_extensions import ParamSpec
|
15
13
|
|
16
14
|
from ostruct import __version__
|
17
15
|
from ostruct.cli.errors import ( # noqa: F401 - Used in error handling
|
18
16
|
SystemPromptError,
|
19
17
|
TaskTemplateVariableError,
|
20
18
|
)
|
19
|
+
from ostruct.cli.validators import (
|
20
|
+
validate_json_variable,
|
21
|
+
validate_name_path_pair,
|
22
|
+
validate_variable,
|
23
|
+
)
|
24
|
+
|
25
|
+
P = ParamSpec("P")
|
26
|
+
R = TypeVar("R")
|
27
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
28
|
+
CommandDecorator = Callable[[F], Command]
|
29
|
+
DecoratedCommand = Union[Command, Callable[..., Any]]
|
21
30
|
|
22
31
|
|
23
|
-
def
|
24
|
-
ctx: click.Context, param: click.Parameter, value: Any
|
25
|
-
) -> Any:
|
26
|
-
"""Validate task-related parameters."""
|
27
|
-
if not hasattr(ctx, "params"):
|
28
|
-
return value
|
29
|
-
|
30
|
-
# Check for conflicting task parameters
|
31
|
-
if (
|
32
|
-
param.name == "task_file"
|
33
|
-
and value is not None
|
34
|
-
and ctx.params.get("task") is not None
|
35
|
-
):
|
36
|
-
raise click.UsageError("Cannot specify both --task and --task-file")
|
37
|
-
elif (
|
38
|
-
param.name == "task"
|
39
|
-
and value is not None
|
40
|
-
and ctx.params.get("task_file") is not None
|
41
|
-
):
|
42
|
-
raise click.UsageError("Cannot specify both --task and --task-file")
|
43
|
-
|
44
|
-
return value
|
45
|
-
|
46
|
-
|
47
|
-
def validate_system_prompt_params(
|
48
|
-
ctx: click.Context, param: click.Parameter, value: Any
|
49
|
-
) -> Any:
|
50
|
-
"""Validate system prompt parameters."""
|
51
|
-
if not hasattr(ctx, "params"):
|
52
|
-
return value
|
53
|
-
|
54
|
-
# Check for conflicting system prompt parameters
|
55
|
-
if (
|
56
|
-
param.name == "system_prompt_file"
|
57
|
-
and value is not None
|
58
|
-
and ctx.params.get("system_prompt") is not None
|
59
|
-
):
|
60
|
-
raise click.UsageError(
|
61
|
-
"Cannot specify both --system-prompt and --system-prompt-file"
|
62
|
-
)
|
63
|
-
elif (
|
64
|
-
param.name == "system_prompt"
|
65
|
-
and value is not None
|
66
|
-
and ctx.params.get("system_prompt_file") is not None
|
67
|
-
):
|
68
|
-
raise click.UsageError(
|
69
|
-
"Cannot specify both --system-prompt and --system-prompt-file"
|
70
|
-
)
|
71
|
-
|
72
|
-
return value
|
73
|
-
|
74
|
-
|
75
|
-
def debug_options(f: Callable) -> Callable:
|
32
|
+
def debug_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
76
33
|
"""Add debug-related CLI options."""
|
77
|
-
|
34
|
+
# Initial conversion to Command if needed
|
35
|
+
cmd: Any = f if isinstance(f, Command) else f
|
36
|
+
|
37
|
+
# Add options without redundant casts
|
38
|
+
cmd = click.option(
|
78
39
|
"--show-model-schema",
|
79
40
|
is_flag=True,
|
80
41
|
help="Show generated Pydantic model schema",
|
81
|
-
)(
|
82
|
-
|
42
|
+
)(cmd)
|
43
|
+
|
44
|
+
cmd = click.option(
|
83
45
|
"--debug-validation",
|
84
46
|
is_flag=True,
|
85
47
|
help="Show detailed validation errors",
|
86
|
-
)(
|
87
|
-
return f
|
48
|
+
)(cmd)
|
88
49
|
|
50
|
+
# Final cast to Command for return type
|
51
|
+
return cast(Command, cmd)
|
89
52
|
|
90
|
-
|
53
|
+
|
54
|
+
def file_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
91
55
|
"""Add file-related CLI options."""
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
"--
|
56
|
+
cmd: Any = f if isinstance(f, Command) else f
|
57
|
+
|
58
|
+
cmd = click.option(
|
59
|
+
"-f",
|
60
|
+
"--file",
|
61
|
+
"files",
|
62
|
+
multiple=True,
|
63
|
+
nargs=2,
|
64
|
+
metavar="<NAME> <PATH>",
|
65
|
+
callback=validate_name_path_pair,
|
66
|
+
help="""Associate a file with a variable name. The file will be available in
|
67
|
+
your template as the specified variable. You can specify this option multiple times.
|
68
|
+
Example: -f code main.py -f test test_main.py""",
|
69
|
+
shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
|
70
|
+
)(cmd)
|
71
|
+
|
72
|
+
cmd = click.option(
|
73
|
+
"-d",
|
74
|
+
"--dir",
|
75
|
+
"dir",
|
97
76
|
multiple=True,
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
"
|
102
|
-
|
103
|
-
|
104
|
-
|
77
|
+
nargs=2,
|
78
|
+
metavar="<NAME> <DIR>",
|
79
|
+
callback=validate_name_path_pair,
|
80
|
+
help="""Associate a directory with a variable name. All files in the directory
|
81
|
+
will be available in your template. Use -R for recursive scanning.
|
82
|
+
Example: -d src ./src""",
|
83
|
+
shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
|
84
|
+
)(cmd)
|
85
|
+
|
86
|
+
cmd = click.option(
|
87
|
+
"-p",
|
88
|
+
"--pattern",
|
89
|
+
"patterns",
|
105
90
|
multiple=True,
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
91
|
+
nargs=2,
|
92
|
+
metavar="<NAME> <PATTERN>",
|
93
|
+
help="""Associate a glob pattern with a variable name. Matching files will be
|
94
|
+
available in your template. Use -R for recursive matching.
|
95
|
+
Example: -p logs '*.log'""",
|
96
|
+
)(cmd)
|
97
|
+
|
98
|
+
cmd = click.option(
|
99
|
+
"-R",
|
100
|
+
"--recursive",
|
101
|
+
is_flag=True,
|
102
|
+
help="Process directories and patterns recursively",
|
103
|
+
)(cmd)
|
104
|
+
|
105
|
+
cmd = click.option(
|
106
|
+
"--base-dir",
|
107
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
108
|
+
help="""Base directory for resolving relative paths. All file operations will be
|
109
|
+
relative to this directory. Defaults to current directory.""",
|
110
|
+
shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
|
111
|
+
)(cmd)
|
112
|
+
|
113
|
+
cmd = click.option(
|
114
|
+
"-A",
|
115
|
+
"--allow",
|
116
|
+
"allowed_dirs",
|
117
|
+
multiple=True,
|
118
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
119
|
+
help="""Add an allowed directory for security. Files must be within allowed
|
120
|
+
directories. Can be specified multiple times.""",
|
121
|
+
shell_complete=click.Path(exists=True, file_okay=False, dir_okay=True),
|
122
|
+
)(cmd)
|
123
|
+
|
124
|
+
cmd = click.option(
|
112
125
|
"--allowed-dir-file",
|
113
|
-
type=
|
114
|
-
help="File containing allowed directory paths
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
def variable_options(f: Callable) -> Callable:
|
126
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
127
|
+
help="""File containing allowed directory paths, one per line. Lines starting
|
128
|
+
with # are treated as comments.""",
|
129
|
+
shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
|
130
|
+
)(cmd)
|
131
|
+
|
132
|
+
return cast(Command, cmd)
|
133
|
+
|
134
|
+
|
135
|
+
def variable_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
126
136
|
"""Add variable-related CLI options."""
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
137
|
+
cmd: Any = f if isinstance(f, Command) else f
|
138
|
+
|
139
|
+
cmd = click.option(
|
140
|
+
"-V",
|
141
|
+
"--var",
|
142
|
+
"var",
|
143
|
+
multiple=True,
|
144
|
+
metavar="name=value",
|
145
|
+
callback=validate_variable,
|
146
|
+
help="""Define a simple string variable. Format: name=value
|
147
|
+
Example: -V debug=true -V env=prod""",
|
148
|
+
)(cmd)
|
149
|
+
|
150
|
+
cmd = click.option(
|
151
|
+
"-J",
|
131
152
|
"--json-var",
|
132
|
-
"
|
153
|
+
"json_var",
|
133
154
|
multiple=True,
|
134
|
-
|
135
|
-
|
136
|
-
|
155
|
+
metavar='name=\'{"json":"value"}\'',
|
156
|
+
callback=validate_json_variable,
|
157
|
+
help="""Define a JSON variable. Format: name='{"key":"value"}'
|
158
|
+
Example: -J config='{"env":"prod","debug":true}'""",
|
159
|
+
)(cmd)
|
160
|
+
|
161
|
+
return cast(Command, cmd)
|
137
162
|
|
138
163
|
|
139
|
-
def model_options(f: Callable) ->
|
164
|
+
def model_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
140
165
|
"""Add model-related CLI options."""
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
"--
|
146
|
-
|
147
|
-
|
148
|
-
"
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
166
|
+
cmd: Any = f if isinstance(f, Command) else f
|
167
|
+
|
168
|
+
cmd = click.option(
|
169
|
+
"-m",
|
170
|
+
"--model",
|
171
|
+
default="gpt-4o",
|
172
|
+
show_default=True,
|
173
|
+
help="""OpenAI model to use. Must support structured output.
|
174
|
+
Supported models:
|
175
|
+
- gpt-4o (128k context window)
|
176
|
+
- o1 (200k context window)
|
177
|
+
- o3-mini (200k context window)""",
|
178
|
+
)(cmd)
|
179
|
+
|
180
|
+
cmd = click.option(
|
181
|
+
"--temperature",
|
182
|
+
type=click.FloatRange(0.0, 2.0),
|
183
|
+
help="""Sampling temperature. Controls randomness in the output.
|
184
|
+
Range: 0.0 to 2.0. Lower values are more focused.""",
|
185
|
+
)(cmd)
|
186
|
+
|
187
|
+
cmd = click.option(
|
188
|
+
"--max-output-tokens",
|
189
|
+
type=click.IntRange(1, None),
|
190
|
+
help="""Maximum number of tokens in the output.
|
191
|
+
Higher values allow longer responses but cost more.""",
|
192
|
+
)(cmd)
|
193
|
+
|
194
|
+
cmd = click.option(
|
195
|
+
"--top-p",
|
196
|
+
type=click.FloatRange(0.0, 1.0),
|
197
|
+
help="""Top-p (nucleus) sampling parameter. Controls diversity.
|
198
|
+
Range: 0.0 to 1.0. Lower values are more focused.""",
|
199
|
+
)(cmd)
|
200
|
+
|
201
|
+
cmd = click.option(
|
154
202
|
"--frequency-penalty",
|
155
|
-
type=
|
156
|
-
|
157
|
-
|
158
|
-
)(
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
203
|
+
type=click.FloatRange(-2.0, 2.0),
|
204
|
+
help="""Frequency penalty for text generation.
|
205
|
+
Range: -2.0 to 2.0. Positive values reduce repetition.""",
|
206
|
+
)(cmd)
|
207
|
+
|
208
|
+
cmd = click.option(
|
209
|
+
"--presence-penalty",
|
210
|
+
type=click.FloatRange(-2.0, 2.0),
|
211
|
+
help="""Presence penalty for text generation.
|
212
|
+
Range: -2.0 to 2.0. Positive values encourage new topics.""",
|
213
|
+
)(cmd)
|
214
|
+
|
215
|
+
cmd = click.option(
|
216
|
+
"--reasoning-effort",
|
217
|
+
type=click.Choice(["low", "medium", "high"]),
|
218
|
+
help="""Control reasoning effort (if supported by model).
|
219
|
+
Higher values may improve output quality but take longer.""",
|
220
|
+
)(cmd)
|
221
|
+
|
222
|
+
return cast(Command, cmd)
|
223
|
+
|
224
|
+
|
225
|
+
def system_prompt_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
226
|
+
"""Add system prompt related CLI options."""
|
227
|
+
cmd: Any = f if isinstance(f, Command) else f
|
228
|
+
|
229
|
+
cmd = click.option(
|
230
|
+
"--sys-prompt",
|
231
|
+
"system_prompt",
|
232
|
+
help="""Provide system prompt directly. This sets the initial context
|
233
|
+
for the model. Example: --sys-prompt "You are a code reviewer." """,
|
234
|
+
)(cmd)
|
235
|
+
|
236
|
+
cmd = click.option(
|
237
|
+
"--sys-file",
|
238
|
+
"system_prompt_file",
|
239
|
+
type=click.Path(exists=True, dir_okay=False),
|
240
|
+
help="""Load system prompt from file. The file should contain the prompt text.
|
241
|
+
Example: --sys-file prompts/code_review.txt""",
|
242
|
+
shell_complete=click.Path(exists=True, file_okay=True, dir_okay=False),
|
243
|
+
)(cmd)
|
244
|
+
|
245
|
+
cmd = click.option(
|
246
|
+
"--ignore-task-sysprompt",
|
247
|
+
is_flag=True,
|
248
|
+
help="""Ignore system prompt in task template. By default, system prompts
|
249
|
+
in template frontmatter are used.""",
|
250
|
+
)(cmd)
|
251
|
+
|
252
|
+
return cast(Command, cmd)
|
253
|
+
|
254
|
+
|
255
|
+
def output_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
256
|
+
"""Add output-related CLI options."""
|
257
|
+
cmd: Any = f if isinstance(f, Command) else f
|
258
|
+
|
259
|
+
cmd = click.option(
|
260
|
+
"--output-file",
|
261
|
+
type=click.Path(dir_okay=False),
|
262
|
+
help="""Write output to file instead of stdout.
|
263
|
+
Example: --output-file result.json""",
|
264
|
+
shell_complete=click.Path(file_okay=True, dir_okay=False),
|
265
|
+
)(cmd)
|
266
|
+
|
267
|
+
cmd = click.option(
|
268
|
+
"--dry-run",
|
269
|
+
is_flag=True,
|
270
|
+
help="""Validate and render but skip API call. Useful for testing
|
271
|
+
template rendering and validation.""",
|
272
|
+
)(cmd)
|
273
|
+
|
274
|
+
return cast(Command, cmd)
|
275
|
+
|
276
|
+
|
277
|
+
def api_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
278
|
+
"""Add API-related CLI options."""
|
279
|
+
cmd: Any = f if isinstance(f, Command) else f
|
280
|
+
|
281
|
+
cmd = click.option(
|
282
|
+
"--api-key",
|
283
|
+
help="""OpenAI API key. If not provided, uses OPENAI_API_KEY
|
284
|
+
environment variable.""",
|
285
|
+
)(cmd)
|
286
|
+
|
287
|
+
cmd = click.option(
|
288
|
+
"--timeout",
|
289
|
+
type=click.FloatRange(1.0, None),
|
290
|
+
default=60.0,
|
291
|
+
show_default=True,
|
292
|
+
help="API timeout in seconds.",
|
293
|
+
)(cmd)
|
294
|
+
|
295
|
+
return cast(Command, cmd)
|
296
|
+
|
297
|
+
|
298
|
+
def debug_progress_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
299
|
+
"""Add debugging and progress CLI options."""
|
300
|
+
cmd: Any = f if isinstance(f, Command) else f
|
301
|
+
|
302
|
+
cmd = click.option(
|
303
|
+
"--no-progress", is_flag=True, help="Disable progress indicators"
|
304
|
+
)(cmd)
|
305
|
+
|
306
|
+
cmd = click.option(
|
307
|
+
"--progress-level",
|
308
|
+
type=click.Choice(["none", "basic", "detailed"]),
|
309
|
+
default="basic",
|
310
|
+
show_default=True,
|
311
|
+
help="""Control progress verbosity. 'none' shows no progress,
|
312
|
+
'basic' shows key steps, 'detailed' shows all steps.""",
|
313
|
+
)(cmd)
|
314
|
+
|
315
|
+
cmd = click.option(
|
316
|
+
"--verbose", is_flag=True, help="Enable verbose logging"
|
317
|
+
)(cmd)
|
318
|
+
|
319
|
+
cmd = click.option(
|
320
|
+
"--debug-openai-stream",
|
321
|
+
is_flag=True,
|
322
|
+
help="Debug OpenAI streaming process",
|
323
|
+
)(cmd)
|
324
|
+
|
325
|
+
return cast(Command, cmd)
|
326
|
+
|
327
|
+
|
328
|
+
def all_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
329
|
+
"""Add all CLI options.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
f: Function to decorate
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
Decorated function
|
336
|
+
"""
|
337
|
+
decorators = [
|
338
|
+
model_options, # Model selection and parameters first
|
339
|
+
system_prompt_options, # System prompt configuration
|
340
|
+
file_options, # File and directory handling
|
341
|
+
variable_options, # Variable definitions
|
342
|
+
output_options, # Output control
|
343
|
+
api_options, # API configuration
|
344
|
+
debug_options, # Debug settings
|
345
|
+
debug_progress_options, # Progress and logging
|
346
|
+
]
|
347
|
+
|
348
|
+
for decorator in decorators:
|
349
|
+
f = decorator(f)
|
350
|
+
|
351
|
+
return cast(Command, f)
|
352
|
+
|
353
|
+
|
354
|
+
def create_click_command() -> CommandDecorator:
|
355
|
+
"""Create the Click command with all options.
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
A decorator function that adds all CLI options to the command.
|
359
|
+
"""
|
360
|
+
|
361
|
+
def decorator(f: F) -> Command:
|
362
|
+
# Initial command creation
|
363
|
+
cmd: Any = click.command()(f)
|
364
|
+
|
365
|
+
# Add version option
|
366
|
+
cmd = click.version_option(
|
367
|
+
__version__,
|
368
|
+
"--version",
|
369
|
+
"-V",
|
370
|
+
message="%(prog)s CLI version %(version)s",
|
371
|
+
)(cmd)
|
372
|
+
|
373
|
+
return cast(Command, cmd)
|
247
374
|
|
248
375
|
return decorator
|