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.
Files changed (112) hide show
  1. cmdbox/__init__.py +0 -0
  2. cmdbox/cli/__init__.py +0 -0
  3. cmdbox/cli/app.py +125 -0
  4. cmdbox/cli/commands/__init__.py +0 -0
  5. cmdbox/cli/commands/alias_fallback.py +102 -0
  6. cmdbox/cli/commands/command_crud.py +429 -0
  7. cmdbox/cli/commands/command_run.py +255 -0
  8. cmdbox/cli/commands/history.py +109 -0
  9. cmdbox/cli/commands/init.py +54 -0
  10. cmdbox/cli/commands/settings.py +62 -0
  11. cmdbox/cli/commands/tag_crud.py +277 -0
  12. cmdbox/cli/commands/variable_crud.py +349 -0
  13. cmdbox/cli/common/__init__.py +0 -0
  14. cmdbox/cli/common/errors.py +58 -0
  15. cmdbox/cli/common/update_fields.py +88 -0
  16. cmdbox/cli/completions/__init__.py +0 -0
  17. cmdbox/cli/completions/commands.py +26 -0
  18. cmdbox/cli/completions/fields.py +31 -0
  19. cmdbox/cli/completions/tags.py +24 -0
  20. cmdbox/cli/completions/variables.py +26 -0
  21. cmdbox/cli/handlers/__init__.py +0 -0
  22. cmdbox/cli/handlers/command_handlers.py +357 -0
  23. cmdbox/cli/handlers/common_handlers.py +15 -0
  24. cmdbox/cli/handlers/history_handlers.py +94 -0
  25. cmdbox/cli/handlers/init_handler.py +127 -0
  26. cmdbox/cli/handlers/run_handler.py +178 -0
  27. cmdbox/cli/handlers/settings_handler.py +59 -0
  28. cmdbox/cli/handlers/tag_handlers.py +220 -0
  29. cmdbox/cli/handlers/variable_handlers.py +272 -0
  30. cmdbox/cli/prompts/__init__.py +0 -0
  31. cmdbox/cli/prompts/completers.py +161 -0
  32. cmdbox/cli/prompts/prompts.py +108 -0
  33. cmdbox/cli/prompts/validators.py +46 -0
  34. cmdbox/cli/ui/__init__.py +0 -0
  35. cmdbox/cli/ui/console.py +31 -0
  36. cmdbox/cli/ui/editor.py +141 -0
  37. cmdbox/cli/ui/presenters/__init__.py +0 -0
  38. cmdbox/cli/ui/presenters/app_presenter.py +8 -0
  39. cmdbox/cli/ui/presenters/command_presenter.py +168 -0
  40. cmdbox/cli/ui/presenters/history_presenter.py +83 -0
  41. cmdbox/cli/ui/presenters/init_instructions.py +52 -0
  42. cmdbox/cli/ui/presenters/init_presenter.py +57 -0
  43. cmdbox/cli/ui/presenters/result_presenter.py +144 -0
  44. cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
  45. cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
  46. cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
  47. cmdbox/cli/ui/primitives.py +410 -0
  48. cmdbox/cli/ui/theme.py +43 -0
  49. cmdbox/cli/ui/theme_builder.py +49 -0
  50. cmdbox/common/__init__.py +0 -0
  51. cmdbox/common/io.py +34 -0
  52. cmdbox/container.py +156 -0
  53. cmdbox/core/__init__.py +0 -0
  54. cmdbox/core/fields.py +48 -0
  55. cmdbox/core/paths.py +52 -0
  56. cmdbox/database.py +65 -0
  57. cmdbox/exceptions.py +10 -0
  58. cmdbox/init/__init__.py +0 -0
  59. cmdbox/init/detect.py +82 -0
  60. cmdbox/init/integrations/bash.sh +10 -0
  61. cmdbox/init/integrations/cmd.bat +14 -0
  62. cmdbox/init/integrations/fish.fish +11 -0
  63. cmdbox/init/integrations/powershell.ps1 +14 -0
  64. cmdbox/init/integrations/zsh.sh +10 -0
  65. cmdbox/init/io.py +68 -0
  66. cmdbox/init/specs.py +54 -0
  67. cmdbox/logging_setup/__init__.py +0 -0
  68. cmdbox/logging_setup/log_config.py +123 -0
  69. cmdbox/logging_setup/log_decorators.py +40 -0
  70. cmdbox/logging_setup/log_handlers.py +94 -0
  71. cmdbox/migrations/__init__.py +1 -0
  72. cmdbox/migrations/errors.py +10 -0
  73. cmdbox/migrations/runner.py +127 -0
  74. cmdbox/migrations/versions/__init__.py +0 -0
  75. cmdbox/models.py +165 -0
  76. cmdbox/repositories/__init__.py +0 -0
  77. cmdbox/repositories/base_repository.py +181 -0
  78. cmdbox/repositories/command_repository.py +391 -0
  79. cmdbox/repositories/errors.py +120 -0
  80. cmdbox/repositories/history_repository.py +155 -0
  81. cmdbox/repositories/results.py +37 -0
  82. cmdbox/repositories/tag_repository.py +91 -0
  83. cmdbox/repositories/validators.py +256 -0
  84. cmdbox/repositories/variable_repository.py +324 -0
  85. cmdbox/resolve/__init__.py +0 -0
  86. cmdbox/resolve/errors.py +65 -0
  87. cmdbox/resolve/lookup.py +137 -0
  88. cmdbox/resolve/resolver.py +402 -0
  89. cmdbox/resolve/type_defs.py +96 -0
  90. cmdbox/runtime/__init__.py +0 -0
  91. cmdbox/runtime/executor.py +454 -0
  92. cmdbox/runtime/results.py +25 -0
  93. cmdbox/runtime/shell.py +90 -0
  94. cmdbox/services/__init__.py +0 -0
  95. cmdbox/services/command_services.py +261 -0
  96. cmdbox/services/errors.py +37 -0
  97. cmdbox/services/field_selection.py +162 -0
  98. cmdbox/services/history_service.py +68 -0
  99. cmdbox/services/run_service.py +204 -0
  100. cmdbox/services/tag_services.py +134 -0
  101. cmdbox/services/variable_services.py +224 -0
  102. cmdbox/settings/__init__.py +0 -0
  103. cmdbox/settings/models.py +129 -0
  104. cmdbox/settings/settings_repository.py +36 -0
  105. cmdbox/settings/settings_service.py +144 -0
  106. cmdbox/version.py +1 -0
  107. cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
  108. cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
  109. cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
  110. cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
  111. cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  112. 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
@@ -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")