cmdbox-cli 1.0.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.
- cmdbox/__init__.py +0 -0
- cmdbox/cli/__init__.py +0 -0
- cmdbox/cli/app.py +125 -0
- cmdbox/cli/commands/__init__.py +0 -0
- cmdbox/cli/commands/alias_fallback.py +102 -0
- cmdbox/cli/commands/command_crud.py +429 -0
- cmdbox/cli/commands/command_run.py +255 -0
- cmdbox/cli/commands/history.py +109 -0
- cmdbox/cli/commands/init.py +54 -0
- cmdbox/cli/commands/settings.py +62 -0
- cmdbox/cli/commands/tag_crud.py +277 -0
- cmdbox/cli/commands/variable_crud.py +349 -0
- cmdbox/cli/common/__init__.py +0 -0
- cmdbox/cli/common/errors.py +58 -0
- cmdbox/cli/common/update_fields.py +88 -0
- cmdbox/cli/completions/__init__.py +0 -0
- cmdbox/cli/completions/commands.py +26 -0
- cmdbox/cli/completions/fields.py +31 -0
- cmdbox/cli/completions/tags.py +24 -0
- cmdbox/cli/completions/variables.py +26 -0
- cmdbox/cli/handlers/__init__.py +0 -0
- cmdbox/cli/handlers/command_handlers.py +357 -0
- cmdbox/cli/handlers/common_handlers.py +15 -0
- cmdbox/cli/handlers/history_handlers.py +94 -0
- cmdbox/cli/handlers/init_handler.py +127 -0
- cmdbox/cli/handlers/run_handler.py +178 -0
- cmdbox/cli/handlers/settings_handler.py +59 -0
- cmdbox/cli/handlers/tag_handlers.py +220 -0
- cmdbox/cli/handlers/variable_handlers.py +272 -0
- cmdbox/cli/prompts/__init__.py +0 -0
- cmdbox/cli/prompts/completers.py +161 -0
- cmdbox/cli/prompts/prompts.py +108 -0
- cmdbox/cli/prompts/validators.py +46 -0
- cmdbox/cli/ui/__init__.py +0 -0
- cmdbox/cli/ui/console.py +31 -0
- cmdbox/cli/ui/editor.py +141 -0
- cmdbox/cli/ui/presenters/__init__.py +0 -0
- cmdbox/cli/ui/presenters/app_presenter.py +8 -0
- cmdbox/cli/ui/presenters/command_presenter.py +168 -0
- cmdbox/cli/ui/presenters/history_presenter.py +83 -0
- cmdbox/cli/ui/presenters/init_instructions.py +52 -0
- cmdbox/cli/ui/presenters/init_presenter.py +57 -0
- cmdbox/cli/ui/presenters/result_presenter.py +144 -0
- cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
- cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
- cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
- cmdbox/cli/ui/primitives.py +410 -0
- cmdbox/cli/ui/theme.py +43 -0
- cmdbox/cli/ui/theme_builder.py +49 -0
- cmdbox/common/__init__.py +0 -0
- cmdbox/common/io.py +34 -0
- cmdbox/container.py +156 -0
- cmdbox/core/__init__.py +0 -0
- cmdbox/core/fields.py +48 -0
- cmdbox/core/paths.py +52 -0
- cmdbox/database.py +65 -0
- cmdbox/exceptions.py +10 -0
- cmdbox/init/__init__.py +0 -0
- cmdbox/init/detect.py +82 -0
- cmdbox/init/integrations/bash.sh +10 -0
- cmdbox/init/integrations/cmd.bat +14 -0
- cmdbox/init/integrations/fish.fish +11 -0
- cmdbox/init/integrations/powershell.ps1 +14 -0
- cmdbox/init/integrations/zsh.sh +10 -0
- cmdbox/init/io.py +68 -0
- cmdbox/init/specs.py +54 -0
- cmdbox/logging_setup/__init__.py +0 -0
- cmdbox/logging_setup/log_config.py +123 -0
- cmdbox/logging_setup/log_decorators.py +40 -0
- cmdbox/logging_setup/log_handlers.py +94 -0
- cmdbox/migrations/__init__.py +1 -0
- cmdbox/migrations/errors.py +10 -0
- cmdbox/migrations/runner.py +127 -0
- cmdbox/migrations/versions/__init__.py +0 -0
- cmdbox/models.py +165 -0
- cmdbox/repositories/__init__.py +0 -0
- cmdbox/repositories/base_repository.py +181 -0
- cmdbox/repositories/command_repository.py +391 -0
- cmdbox/repositories/errors.py +120 -0
- cmdbox/repositories/history_repository.py +155 -0
- cmdbox/repositories/results.py +37 -0
- cmdbox/repositories/tag_repository.py +91 -0
- cmdbox/repositories/validators.py +256 -0
- cmdbox/repositories/variable_repository.py +324 -0
- cmdbox/resolve/__init__.py +0 -0
- cmdbox/resolve/errors.py +65 -0
- cmdbox/resolve/lookup.py +137 -0
- cmdbox/resolve/resolver.py +402 -0
- cmdbox/resolve/type_defs.py +96 -0
- cmdbox/runtime/__init__.py +0 -0
- cmdbox/runtime/executor.py +454 -0
- cmdbox/runtime/results.py +25 -0
- cmdbox/runtime/shell.py +90 -0
- cmdbox/services/__init__.py +0 -0
- cmdbox/services/command_services.py +261 -0
- cmdbox/services/errors.py +37 -0
- cmdbox/services/field_selection.py +162 -0
- cmdbox/services/history_service.py +68 -0
- cmdbox/services/run_service.py +204 -0
- cmdbox/services/tag_services.py +134 -0
- cmdbox/services/variable_services.py +224 -0
- cmdbox/settings/__init__.py +0 -0
- cmdbox/settings/models.py +129 -0
- cmdbox/settings/settings_repository.py +36 -0
- cmdbox/settings/settings_service.py +144 -0
- cmdbox/version.py +1 -0
- cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
- cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
- cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
- cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Callable, Any, Sequence, Dict
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from cmdbox.cli.common.update_fields import (
|
|
7
|
+
merge_fields,
|
|
8
|
+
parse_set_pairs,
|
|
9
|
+
filter_allowed,
|
|
10
|
+
)
|
|
11
|
+
from cmdbox.cli.handlers.common_handlers import get_tags_interactive
|
|
12
|
+
from cmdbox.cli.prompts.prompts import (
|
|
13
|
+
prompt_for_name,
|
|
14
|
+
prompt_for_value,
|
|
15
|
+
)
|
|
16
|
+
from cmdbox.cli.prompts.validators import NameValidator
|
|
17
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
18
|
+
from cmdbox.cli.ui.presenters.variable_presenter import (
|
|
19
|
+
render_variable_created,
|
|
20
|
+
render_variable,
|
|
21
|
+
render_variable_list,
|
|
22
|
+
render_variable_updated,
|
|
23
|
+
render_variable_deleted,
|
|
24
|
+
)
|
|
25
|
+
from cmdbox.cli.ui.presenters.result_presenter import (
|
|
26
|
+
render_tag_attach_result,
|
|
27
|
+
render_tag_detach_result,
|
|
28
|
+
)
|
|
29
|
+
from cmdbox.services.field_selection import FieldSelectionResolver
|
|
30
|
+
from cmdbox.services.variable_services import VariableServices
|
|
31
|
+
from cmdbox.services.tag_services import TagServices
|
|
32
|
+
from cmdbox.settings.models import Settings
|
|
33
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class AddVariableArgs:
|
|
38
|
+
name: Optional[str]
|
|
39
|
+
value: Optional[str]
|
|
40
|
+
tags: Optional[list[str]]
|
|
41
|
+
interactive: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@log_action(__name__, "run_add_variable")
|
|
45
|
+
def run_add_variable(
|
|
46
|
+
*,
|
|
47
|
+
args: AddVariableArgs,
|
|
48
|
+
get_var_services: Callable[[], VariableServices],
|
|
49
|
+
get_tag_services: Callable[[], TagServices],
|
|
50
|
+
get_console: Callable[[], ConsoleUI],
|
|
51
|
+
) -> None:
|
|
52
|
+
name = args.name
|
|
53
|
+
value = args.value
|
|
54
|
+
tags = args.tags
|
|
55
|
+
|
|
56
|
+
if args.interactive or name is None:
|
|
57
|
+
name = prompt_for_name(NameValidator())
|
|
58
|
+
|
|
59
|
+
if args.interactive or value is None:
|
|
60
|
+
value = prompt_for_value()
|
|
61
|
+
|
|
62
|
+
if args.interactive or tags is None:
|
|
63
|
+
tags = get_tags_interactive(get_tag_services())
|
|
64
|
+
if not tags:
|
|
65
|
+
tags = None
|
|
66
|
+
|
|
67
|
+
var_service = get_var_services()
|
|
68
|
+
var = var_service.create_variable(name=name, value=value, tags=tags)
|
|
69
|
+
console = get_console()
|
|
70
|
+
console.print(render_variable_created(var))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@log_action(__name__, "run_get_variable")
|
|
74
|
+
def run_get_variable(
|
|
75
|
+
*,
|
|
76
|
+
name: str,
|
|
77
|
+
get_var_services: Callable[[], VariableServices],
|
|
78
|
+
get_console: Callable[[], ConsoleUI],
|
|
79
|
+
) -> None:
|
|
80
|
+
console = get_console()
|
|
81
|
+
var_service = get_var_services()
|
|
82
|
+
var = var_service.get_variable(name)
|
|
83
|
+
rendered_var = render_variable(var)
|
|
84
|
+
console.print(rendered_var)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@log_action(__name__, "run_update_variable")
|
|
88
|
+
def run_update_variable(
|
|
89
|
+
*,
|
|
90
|
+
name: str,
|
|
91
|
+
value: Optional[str],
|
|
92
|
+
new_name: Optional[str],
|
|
93
|
+
set_pairs: Optional[Sequence[str]],
|
|
94
|
+
edit_mode: bool,
|
|
95
|
+
edit_fields: Optional[str],
|
|
96
|
+
get_var_services: Callable[[], VariableServices],
|
|
97
|
+
get_settings: Callable[[], Settings],
|
|
98
|
+
get_console: Callable[[], ConsoleUI],
|
|
99
|
+
) -> None:
|
|
100
|
+
allowed = {"name", "value"}
|
|
101
|
+
fields: Dict[str, Any] = {}
|
|
102
|
+
|
|
103
|
+
var_service = get_var_services()
|
|
104
|
+
var = var_service.get_variable(name)
|
|
105
|
+
console = get_console()
|
|
106
|
+
|
|
107
|
+
if edit_mode:
|
|
108
|
+
if any([new_name, value, set_pairs]):
|
|
109
|
+
raise typer.BadParameter(
|
|
110
|
+
"--edit cannot be combined with field options or --set."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
updated_fields: dict[str, Any] = {}
|
|
114
|
+
if edit_fields:
|
|
115
|
+
edit_fields = [x.strip() for x in edit_fields.split(",")]
|
|
116
|
+
|
|
117
|
+
field_aliases = get_settings().field_aliases.alias_mapping
|
|
118
|
+
|
|
119
|
+
def check_field_aliases(field: str) -> bool:
|
|
120
|
+
return (
|
|
121
|
+
edit_fields is None
|
|
122
|
+
or field in edit_fields
|
|
123
|
+
or any(x in edit_fields for x in field_aliases.get(field, []))
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if check_field_aliases("name"):
|
|
127
|
+
updated_fields["name"] = prompt_for_name(NameValidator(), default=var.name)
|
|
128
|
+
if check_field_aliases("value"):
|
|
129
|
+
updated_fields["value"] = prompt_for_value(default=var.value)
|
|
130
|
+
|
|
131
|
+
fields = merge_fields(fields, updated_fields)
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
if value is not None:
|
|
135
|
+
fields["value"] = value
|
|
136
|
+
if new_name is not None:
|
|
137
|
+
fields["name"] = new_name
|
|
138
|
+
|
|
139
|
+
fields = merge_fields(fields, parse_set_pairs(set_pairs))
|
|
140
|
+
fields = filter_allowed(fields, allowed)
|
|
141
|
+
|
|
142
|
+
if not fields:
|
|
143
|
+
raise typer.BadParameter("No fields specified to update.")
|
|
144
|
+
|
|
145
|
+
current = {
|
|
146
|
+
"name": var.name,
|
|
147
|
+
"value": var.value,
|
|
148
|
+
}
|
|
149
|
+
fields = {key: value for key, value in fields.items() if current.get(key) != value}
|
|
150
|
+
|
|
151
|
+
if not fields:
|
|
152
|
+
console.info("No changes detected.")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
var_service.update_variable(name, **fields)
|
|
156
|
+
|
|
157
|
+
updated_var = var_service.get_variable_by_id(var.id)
|
|
158
|
+
console.print(render_variable_updated(updated_var))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@log_action(__name__, "run_list_variables")
|
|
162
|
+
def run_list_variables(
|
|
163
|
+
*,
|
|
164
|
+
limit: int,
|
|
165
|
+
order_by: str,
|
|
166
|
+
tags: list[str] | None,
|
|
167
|
+
fields: list[str] | None = None,
|
|
168
|
+
get_var_services: Callable[[], VariableServices],
|
|
169
|
+
get_settings: Callable[[], Settings],
|
|
170
|
+
get_console: Callable[[], ConsoleUI],
|
|
171
|
+
get_display_field_resolver: Callable[[], FieldSelectionResolver],
|
|
172
|
+
) -> None:
|
|
173
|
+
console = get_console()
|
|
174
|
+
var_service = get_var_services()
|
|
175
|
+
vars_ = var_service.list_variables(limit=limit, order_by=order_by, tags=tags)
|
|
176
|
+
|
|
177
|
+
settings = get_settings()
|
|
178
|
+
fields = get_display_field_resolver().resolve(
|
|
179
|
+
fields,
|
|
180
|
+
default_fields=settings.default_fields.variable_output,
|
|
181
|
+
aliases=settings.field_aliases.alias_map,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(render_variable_list(vars_, title="Variables", fields=fields))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@log_action(__name__, "run_search_variables")
|
|
188
|
+
def run_search_variables(
|
|
189
|
+
*,
|
|
190
|
+
term: str,
|
|
191
|
+
limit: int,
|
|
192
|
+
search_fields: list[str] | None = None,
|
|
193
|
+
fields: list[str] | None = None,
|
|
194
|
+
get_var_services: Callable[[], VariableServices],
|
|
195
|
+
get_settings: Callable[[], Settings],
|
|
196
|
+
get_console: Callable[[], ConsoleUI],
|
|
197
|
+
get_display_field_resolver: Callable[[], FieldSelectionResolver],
|
|
198
|
+
get_search_field_resolver: Callable[[], FieldSelectionResolver],
|
|
199
|
+
) -> None:
|
|
200
|
+
console = get_console()
|
|
201
|
+
var_service = get_var_services()
|
|
202
|
+
|
|
203
|
+
settings = get_settings()
|
|
204
|
+
output_fields = get_display_field_resolver().resolve(
|
|
205
|
+
fields,
|
|
206
|
+
default_fields=settings.default_fields.variable_output,
|
|
207
|
+
aliases=settings.field_aliases.alias_map,
|
|
208
|
+
)
|
|
209
|
+
search_fields = get_search_field_resolver().resolve(
|
|
210
|
+
search_fields,
|
|
211
|
+
default_fields=settings.default_fields.variable_search,
|
|
212
|
+
aliases=settings.field_aliases.alias_map,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
vars_ = var_service.search(term, limit=limit, fields=search_fields)
|
|
216
|
+
console.print(
|
|
217
|
+
render_variable_list(vars_, title="Search Results", fields=output_fields)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@log_action(__name__, "run_delete_variable")
|
|
222
|
+
def run_delete_variable(
|
|
223
|
+
*,
|
|
224
|
+
name: str,
|
|
225
|
+
get_var_services: Callable[[], VariableServices],
|
|
226
|
+
get_console: Callable[[], ConsoleUI],
|
|
227
|
+
) -> None:
|
|
228
|
+
console = get_console()
|
|
229
|
+
var_service = get_var_services()
|
|
230
|
+
var = var_service.get_variable(name)
|
|
231
|
+
if var_service.delete_variable(name):
|
|
232
|
+
console.print(render_variable_deleted(var))
|
|
233
|
+
else:
|
|
234
|
+
console.error(f"Failed to delete variable '{name}'.")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@log_action(__name__, "run_attach_tags")
|
|
238
|
+
def run_attach_tags(
|
|
239
|
+
*,
|
|
240
|
+
name: str | None = None,
|
|
241
|
+
tag_names: list[str] | None = None,
|
|
242
|
+
get_var_services: Callable[[], VariableServices],
|
|
243
|
+
get_tag_services: Callable[[], TagServices],
|
|
244
|
+
get_console: Callable[[], ConsoleUI],
|
|
245
|
+
) -> None:
|
|
246
|
+
if not name:
|
|
247
|
+
name = prompt_for_name(NameValidator())
|
|
248
|
+
if not tag_names:
|
|
249
|
+
tag_names = get_tags_interactive(get_tag_services())
|
|
250
|
+
var_service = get_var_services()
|
|
251
|
+
result = var_service.add_tags(name=name, tags=tag_names)
|
|
252
|
+
console = get_console()
|
|
253
|
+
console.print(render_tag_attach_result(result))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@log_action(__name__, "run_detach_tags")
|
|
257
|
+
def run_detach_tags(
|
|
258
|
+
*,
|
|
259
|
+
name: str | None = None,
|
|
260
|
+
tag_names: list[str] | None = None,
|
|
261
|
+
get_var_services: Callable[[], VariableServices],
|
|
262
|
+
get_tag_services: Callable[[], TagServices],
|
|
263
|
+
get_console: Callable[[], ConsoleUI],
|
|
264
|
+
) -> None:
|
|
265
|
+
if not name:
|
|
266
|
+
name = prompt_for_name(NameValidator())
|
|
267
|
+
if not tag_names:
|
|
268
|
+
tag_names = get_tags_interactive(get_tag_services())
|
|
269
|
+
var_service = get_var_services()
|
|
270
|
+
result = var_service.remove_tags(name=name, tags=tag_names)
|
|
271
|
+
console = get_console()
|
|
272
|
+
console.print(render_tag_detach_result(result))
|
|
File without changes
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable, Sequence, Iterable, Tuple
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit.completion import Completer, Completion, CompleteEvent
|
|
5
|
+
from prompt_toolkit.document import Document
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _normalize_tag(s: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Normalizes a tag by stripping leading and trailing whitespace and converting the string to lowercase.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
s (str): The string to be normalized.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
str: The normalized string.
|
|
17
|
+
"""
|
|
18
|
+
return s.strip().lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _split_csv_like(text: str) -> Tuple[str, str, int]:
|
|
22
|
+
"""
|
|
23
|
+
Split input into:
|
|
24
|
+
- prefix: Everything up to and including the last comma (and any following spaces)
|
|
25
|
+
- active: The chunk the user is currently typing (no comma)
|
|
26
|
+
- start_pos: The start index of active within the full text
|
|
27
|
+
Args:
|
|
28
|
+
text:
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
last_comma = text.rfind(",")
|
|
34
|
+
if last_comma == -1:
|
|
35
|
+
prefix = ""
|
|
36
|
+
active = text
|
|
37
|
+
start_pos = 0
|
|
38
|
+
return prefix, active, start_pos
|
|
39
|
+
|
|
40
|
+
prefix_end = last_comma + 1
|
|
41
|
+
prefix = text[:prefix_end]
|
|
42
|
+
i = prefix_end
|
|
43
|
+
while i < len(text) and text[i].isspace():
|
|
44
|
+
i += 1
|
|
45
|
+
prefix = text[:i]
|
|
46
|
+
active = text[i:]
|
|
47
|
+
start_pos = i
|
|
48
|
+
return prefix, active, start_pos
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _simple_fuzzy_score(needle: str, haystack: str) -> int:
|
|
52
|
+
"""
|
|
53
|
+
Lightweight fuzzy score with no external dependencies.
|
|
54
|
+
Higher is better. Range is 0...100.
|
|
55
|
+
Args:
|
|
56
|
+
needle: The search term to match within the haystack
|
|
57
|
+
haystack: The text to search for needle within
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
An integer score indicating how well needle matches haystack
|
|
61
|
+
"""
|
|
62
|
+
if not needle:
|
|
63
|
+
return 1
|
|
64
|
+
if haystack.startswith(needle):
|
|
65
|
+
return 100
|
|
66
|
+
if needle in haystack:
|
|
67
|
+
return 75
|
|
68
|
+
it = iter(haystack)
|
|
69
|
+
if all(ch in it for ch in needle):
|
|
70
|
+
return 55
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class TagCompleterConfig:
|
|
76
|
+
min_score: int = 1
|
|
77
|
+
max_results: int = 12
|
|
78
|
+
case_insensitive: bool = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TagCompleter(Completer):
|
|
82
|
+
"""
|
|
83
|
+
Implements a tag completion mechanism for input fields.
|
|
84
|
+
|
|
85
|
+
This class provides dynamic suggestions for completing partial user inputs based on a
|
|
86
|
+
predefined pool of tags. The suggestions are context-aware, configurable, and sorted
|
|
87
|
+
by relevance. It is specifically designed for scenarios where inputs consist of a
|
|
88
|
+
comma-separated list of tags.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
_get_tags (Callable[[str], Sequence[str]]): A function that retrieves the list of
|
|
92
|
+
available tags based on the search query provided.
|
|
93
|
+
_config (TagCompleterConfig): Configuration object that determines completion
|
|
94
|
+
behavior, including case sensitivity, scoring thresholds, and maximum results.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
get_tags: Callable[[str], Sequence[str]],
|
|
100
|
+
config: TagCompleterConfig | None = None,
|
|
101
|
+
):
|
|
102
|
+
self._get_tags = get_tags
|
|
103
|
+
self._config = config if config is not None else TagCompleterConfig()
|
|
104
|
+
|
|
105
|
+
def get_completions(
|
|
106
|
+
self, document: Document, complete_event: CompleteEvent
|
|
107
|
+
) -> Iterable[Completion]:
|
|
108
|
+
"""
|
|
109
|
+
Generate completions for a partial text input based on a list of pre-defined tags.
|
|
110
|
+
|
|
111
|
+
This method processes the input text before the cursor, normalizes it based on
|
|
112
|
+
configuration settings, and evaluates potential completions from a pool of tags.
|
|
113
|
+
Completions are filtered and sorted by relevance, and only unique, unused tags
|
|
114
|
+
meeting a minimum score threshold are suggested.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
document (Document): The input context containing the text typed by the user.
|
|
118
|
+
complete_event (CompleteEvent): Event instance providing additional
|
|
119
|
+
information about the completion trigger.
|
|
120
|
+
|
|
121
|
+
Yields:
|
|
122
|
+
Completion: A generator of Completion objects, each containing a potential
|
|
123
|
+
completion string, its display text, and the position within the text
|
|
124
|
+
where it should be applied.
|
|
125
|
+
"""
|
|
126
|
+
text = document.text_before_cursor
|
|
127
|
+
prefix, active, active_start = _split_csv_like(text)
|
|
128
|
+
|
|
129
|
+
active_norm = (
|
|
130
|
+
_normalize_tag(active) if self._config.case_insensitive else active.strip()
|
|
131
|
+
)
|
|
132
|
+
if self._config.case_insensitive:
|
|
133
|
+
tag_pool = [(t, _normalize_tag(t)) for t in self._get_tags(active)]
|
|
134
|
+
else:
|
|
135
|
+
tag_pool = [(t, t.strip()) for t in self._get_tags(active)]
|
|
136
|
+
|
|
137
|
+
# Do not suggest tags already used in the list
|
|
138
|
+
already_raw = prefix.split(",")
|
|
139
|
+
already = {_normalize_tag(t) for t in already_raw if t.strip()}
|
|
140
|
+
scored: list[Tuple[int, str]] = []
|
|
141
|
+
|
|
142
|
+
for original, norm in tag_pool:
|
|
143
|
+
if not norm:
|
|
144
|
+
continue
|
|
145
|
+
if norm in already:
|
|
146
|
+
continue
|
|
147
|
+
score = _simple_fuzzy_score(active_norm, norm)
|
|
148
|
+
if score >= self._config.min_score:
|
|
149
|
+
scored.append((score, original))
|
|
150
|
+
|
|
151
|
+
scored.sort(key=lambda x: (-x[0], x[1].lower()))
|
|
152
|
+
# Replace only the active chunk, not the entire line
|
|
153
|
+
# Negative chars to delete before inserting completion
|
|
154
|
+
start_pos = -len(active)
|
|
155
|
+
|
|
156
|
+
for score, tag in scored[: self._config.max_results]:
|
|
157
|
+
yield Completion(
|
|
158
|
+
text=tag,
|
|
159
|
+
start_position=start_pos,
|
|
160
|
+
display=tag,
|
|
161
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from prompt_toolkit import prompt
|
|
3
|
+
|
|
4
|
+
from cmdbox.cli.prompts.completers import TagCompleter
|
|
5
|
+
from cmdbox.cli.prompts.validators import (
|
|
6
|
+
AliasValidator,
|
|
7
|
+
TemplateValidator,
|
|
8
|
+
TagNameValidator,
|
|
9
|
+
NameValidator,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prompt_for_confirm(message: str, default: bool = False) -> bool:
|
|
14
|
+
suffix = " [Y/n]: " if default else " [y/N]: "
|
|
15
|
+
result = prompt(message + suffix).strip().lower()
|
|
16
|
+
if result == "":
|
|
17
|
+
return default
|
|
18
|
+
return result in ["y", "yes"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def prompt_for_alias(validator: AliasValidator, default: str = "") -> str:
|
|
22
|
+
alias = prompt("Enter alias: ", validator=validator, default=default)
|
|
23
|
+
return alias
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prompt_for_template(validator: TemplateValidator, default: str = "") -> str:
|
|
27
|
+
template = prompt(
|
|
28
|
+
"Enter template: ",
|
|
29
|
+
prompt_continuation=" ... ",
|
|
30
|
+
multiline=True,
|
|
31
|
+
validator=validator,
|
|
32
|
+
default=default,
|
|
33
|
+
)
|
|
34
|
+
return template
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def prompt_for_description(default: str = "") -> str:
|
|
38
|
+
description = prompt("Enter description: ", default=default)
|
|
39
|
+
return description
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def prompt_for_cwd(default: str = "") -> str:
|
|
43
|
+
cwd = prompt("Enter working directory: ", default=default)
|
|
44
|
+
return cwd
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prompt_for_shell(default: str = "") -> str:
|
|
48
|
+
shell = prompt("Enter shell: ", default=default)
|
|
49
|
+
return shell
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def prompt_for_timeout(default: str = "") -> str:
|
|
53
|
+
timeout = prompt("Enter timeout (in seconds): ", default=default)
|
|
54
|
+
return timeout
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def prompt_for_name(
|
|
58
|
+
validator: NameValidator | TagNameValidator, default: str = ""
|
|
59
|
+
) -> str:
|
|
60
|
+
name = prompt("Enter name: ", validator=validator, default=default)
|
|
61
|
+
return name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def prompt_for_value(default: str = "") -> str:
|
|
65
|
+
value = prompt("Enter variable value: ", default=default)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def prompt_for_tags(
|
|
70
|
+
tag_completer: TagCompleter, validator: TagNameValidator, default: str = ""
|
|
71
|
+
) -> list[str] | None:
|
|
72
|
+
tags = prompt(
|
|
73
|
+
"Enter tags (comma-separated): ",
|
|
74
|
+
completer=tag_completer,
|
|
75
|
+
validator=validator,
|
|
76
|
+
default=default,
|
|
77
|
+
)
|
|
78
|
+
if not tags:
|
|
79
|
+
return None
|
|
80
|
+
return tags.split(",")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def prompt_for_missing_var(var_name: str) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Prompts the user to input a value for a given variable name.
|
|
86
|
+
|
|
87
|
+
This function checks if the standard output is a terminal. If it is, the function
|
|
88
|
+
uses the `prompt()` mechanism to get the input. If standard output is not a terminal
|
|
89
|
+
(e.g., the command is being run in cbe emit mode), the function writes the prompt to
|
|
90
|
+
standard error, flushes the stream, and then reads input directly from the console.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
var_name: A string representing the name of the variable for which a value
|
|
94
|
+
is being requested.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A string containing the value entered by the user for the specified variable.
|
|
98
|
+
"""
|
|
99
|
+
message = f"Enter value for <{var_name}>: "
|
|
100
|
+
|
|
101
|
+
# Normal execution
|
|
102
|
+
if sys.stdout.isatty():
|
|
103
|
+
return prompt(message)
|
|
104
|
+
|
|
105
|
+
# Stdout is a pipe, not tty (e.g. cbe emit mode) - open console directly
|
|
106
|
+
sys.stderr.write(message)
|
|
107
|
+
sys.stderr.flush()
|
|
108
|
+
return sys.stdin.readline().strip("\n")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from prompt_toolkit.document import Document
|
|
2
|
+
from prompt_toolkit.validation import Validator, ValidationError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AliasValidator(Validator):
|
|
6
|
+
|
|
7
|
+
def validate(self, document: Document) -> None:
|
|
8
|
+
text = document.text.strip()
|
|
9
|
+
if text == "":
|
|
10
|
+
raise ValidationError(message="Alias cannot be empty")
|
|
11
|
+
if " " in text:
|
|
12
|
+
raise ValidationError(message="Alias cannot contain spaces")
|
|
13
|
+
if "<" in text or ">" in text:
|
|
14
|
+
raise ValidationError(message="Alias cannot contain '<' or '>' characters")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TemplateValidator(Validator):
|
|
18
|
+
|
|
19
|
+
def validate(self, document: Document) -> None:
|
|
20
|
+
text = document.text.strip()
|
|
21
|
+
if text == "":
|
|
22
|
+
raise ValidationError(message="Template cannot be empty")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NameValidator(Validator):
|
|
26
|
+
|
|
27
|
+
def validate(self, document: Document) -> None:
|
|
28
|
+
text = document.text.strip()
|
|
29
|
+
if text == "":
|
|
30
|
+
raise ValidationError(message="Name cannot be empty")
|
|
31
|
+
if " " in text:
|
|
32
|
+
raise ValidationError(message="Name cannot contain spaces")
|
|
33
|
+
if "<" in text or ">" in text:
|
|
34
|
+
raise ValidationError(message="Name cannot contain '<' or '>' characters")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TagNameValidator(Validator):
|
|
38
|
+
|
|
39
|
+
def validate(self, document: Document) -> None:
|
|
40
|
+
text = document.text.strip()
|
|
41
|
+
if " " in text:
|
|
42
|
+
raise ValidationError(message="Tag name cannot contain spaces")
|
|
43
|
+
if "<" in text or ">" in text:
|
|
44
|
+
raise ValidationError(
|
|
45
|
+
message="Tag name cannot contain '<' or '>' characters"
|
|
46
|
+
)
|
|
File without changes
|
cmdbox/cli/ui/console.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConsoleUI:
|
|
5
|
+
|
|
6
|
+
def __init__(self, theme, *, force_color=None):
|
|
7
|
+
self._console = Console(
|
|
8
|
+
theme=theme, force_terminal=force_color, highlight=False
|
|
9
|
+
)
|
|
10
|
+
self._theme = theme
|
|
11
|
+
|
|
12
|
+
def print(self, thing) -> None:
|
|
13
|
+
self._console.print(thing)
|
|
14
|
+
|
|
15
|
+
def success(self, message: str) -> None:
|
|
16
|
+
self._console.print(message, style="status.success")
|
|
17
|
+
|
|
18
|
+
def warning(self, message: str) -> None:
|
|
19
|
+
self._console.print(message, style="status.warning")
|
|
20
|
+
|
|
21
|
+
def error(self, message: str) -> None:
|
|
22
|
+
self._console.print(message, style="status.error")
|
|
23
|
+
|
|
24
|
+
def info(self, message: str) -> None:
|
|
25
|
+
self._console.print(message, style="status.info")
|
|
26
|
+
|
|
27
|
+
def muted(self, message: str) -> None:
|
|
28
|
+
self._console.print(message, style="status.muted")
|
|
29
|
+
|
|
30
|
+
def debug(self, message: str) -> None:
|
|
31
|
+
self._console.print(message, style="status.debug")
|