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,88 @@
1
+ from typing import Any, Optional, Iterable, Dict
2
+ import typer
3
+
4
+
5
+ def parse_set_pairs(pairs: Optional[list[str]]) -> Dict[str, str]:
6
+ """
7
+ Parses a list of key-value string pairs and converts them into a dictionary.
8
+
9
+ This function takes a list of strings formatted as "key=value", validates their structure,
10
+ and converts them into a dictionary where the keys and values correspond to the
11
+ parsed components of the strings. Invalid pairs result in an error being raised.
12
+
13
+ Args:
14
+ pairs (Optional[list[str]]): A list of strings where each string is formatted
15
+ as "key=value". If the list is empty or None, an empty dictionary is returned.
16
+
17
+ Returns:
18
+ Dict[str, str]: A dictionary where the keys and values correspond to the parsed
19
+ key-value pairs from the input list.
20
+
21
+ Raises:
22
+ typer.BadParameter: If any string in the list does not contain an "=" character,
23
+ or if the key part is empty after splitting.
24
+ """
25
+ out: Dict[str, str] = {}
26
+ if not pairs:
27
+ return out
28
+ for item in pairs:
29
+ if "=" not in item:
30
+ raise typer.BadParameter(f"Invalid --set value '{item}'. Use key=value.")
31
+ k, v = item.split("=", 1)
32
+ k = k.strip()
33
+ if not k:
34
+ raise typer.BadParameter(
35
+ f"Invalid --set value '{item}'. Key cannot be empty."
36
+ )
37
+ out[k] = v
38
+ return out
39
+
40
+
41
+ def merge_fields(base: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:
42
+ """
43
+ Merges two dictionaries and ensures there are no overlapping keys. If overlapping keys
44
+ are detected, an error is raised with a detailed message indicating the conflicts.
45
+
46
+ Args:
47
+ base (Dict[str, Any]): The base dictionary to merge into.
48
+ extra (Dict[str, Any]): The additional dictionary containing keys and values to
49
+ merge into the base dictionary.
50
+
51
+ Returns:
52
+ Dict[str, Any]: A new dictionary with the combined keys and values from the
53
+ `base` and `extra` dictionaries.
54
+
55
+ Raises:
56
+ typer.BadParameter: If the `base` and `extra` dictionaries have overlapping keys.
57
+ """
58
+ conflicts = set(base).intersection(extra)
59
+ if conflicts:
60
+ keys = ", ".join(sorted(conflicts))
61
+ raise typer.BadParameter(f"Field(s) specified multiple ways: {keys}")
62
+ merged = dict(base)
63
+ merged.update(extra)
64
+ return merged
65
+
66
+
67
+ def filter_allowed(fields: Dict[str, Any], allowed: Iterable[str]) -> Dict[str, Any]:
68
+ """
69
+ Filters a dictionary to retain only allowed keys and raises an error if unknown keys are
70
+ present in the dictionary.
71
+
72
+ Args:
73
+ fields (Dict[str, Any]): A dictionary containing the fields to filter.
74
+ allowed (Iterable[str]): An iterable of allowed keys.
75
+
76
+ Returns:
77
+ Dict[str, Any]: A dictionary containing only the fields with keys found in the allowed
78
+ set.
79
+
80
+ Raises:
81
+ typer.BadParameter: If the dictionary contains keys not found in the allowed set.
82
+ """
83
+ allowed_set = set(allowed)
84
+ unknown = [k for k in fields.keys() if k not in allowed_set]
85
+ if unknown:
86
+ keys = ", ".join(sorted(unknown))
87
+ raise typer.BadParameter(f"Unknown field(s) for --set: {keys}")
88
+ return fields
File without changes
@@ -0,0 +1,26 @@
1
+ def complete_command_aliases(incomplete: str) -> list[str]:
2
+ """
3
+ Completes a list of command aliases based on the provided partial string.
4
+
5
+ This function provides completion suggestions for command aliases. If the
6
+ input string is empty, it returns a default list of command aliases ordered
7
+ by their last updated timestamps. Otherwise, it performs a search based on
8
+ the provided input string to retrieve relevant command aliases.
9
+
10
+ Args:
11
+ incomplete (str): The partial command alias string to search for. If an
12
+ empty string is provided, the function returns a list of the most
13
+ updated command aliases.
14
+
15
+ Returns:
16
+ list[str]: A list of command aliases that match the input string or
17
+ the most recently updated aliases if the input is empty.
18
+ """
19
+ from cmdbox.container import get_command_services
20
+
21
+ svc = get_command_services()
22
+ if incomplete == "":
23
+ cmds = svc.list_commands(order_by="last_updated", limit=20)
24
+ else:
25
+ cmds = svc.search(incomplete, fields="alias", limit=20)
26
+ return [x.alias for x in cmds]
@@ -0,0 +1,31 @@
1
+ from cmdbox.models import Command, Variable, Tag
2
+
3
+
4
+ def command_field_options(incomplete: str) -> list[str]:
5
+ fields = Command._meta.sorted_field_names
6
+ return [field for field in fields if field.startswith(incomplete)]
7
+
8
+
9
+ def command_editable_field_options(incomplete: str) -> list[str]:
10
+ fields = ["alias", "template", "description"]
11
+ return [field for field in fields if field.startswith(incomplete)]
12
+
13
+
14
+ def variable_field_options(incomplete: str) -> list[str]:
15
+ fields = Variable._meta.sorted_field_names
16
+ return [field for field in fields if field.startswith(incomplete)]
17
+
18
+
19
+ def variable_editable_field_options(incomplete: str) -> list[str]:
20
+ fields = ["name", "value"]
21
+ return [field for field in fields if field.startswith(incomplete)]
22
+
23
+
24
+ def tag_field_options(incomplete: str) -> list[str]:
25
+ fields = Tag._meta.sorted_field_names
26
+ return [field for field in fields if field.startswith(incomplete)]
27
+
28
+
29
+ def tag_editable_field_options(incomplete: str) -> list[str]:
30
+ fields = ["name", "description"]
31
+ return [field for field in fields if field.startswith(incomplete)]
@@ -0,0 +1,24 @@
1
+ def complete_tag_names(incomplete: str) -> list[str]:
2
+ """
3
+ Completes a list of tag names based on the provided partial string.
4
+
5
+ This function provides completion suggestions for tag names. If the input string is empty,
6
+ it returns a default list of tag names ordered by their last updated timestamps. Otherwise,
7
+ it performs a search for tags that match or are derived from the given incomplete string.
8
+
9
+ Args:
10
+ incomplete (str): The partial tag string to search for. If an empty string is provided,
11
+ the function returns a list of the most recently updated tags.
12
+
13
+ Returns:
14
+ list[str]: A list of tag names matching or related to the given incomplete string.
15
+ """
16
+ from cmdbox.container import get_tag_services
17
+
18
+ svc = get_tag_services()
19
+
20
+ if incomplete == "":
21
+ tags = svc.list_tags(order_by="last_updated", limit=20)
22
+ else:
23
+ tags = svc.search(incomplete, fields="name", limit=20)
24
+ return [tag.name for tag in tags]
@@ -0,0 +1,26 @@
1
+ def complete_variable_names(incomplete: str) -> list[str]:
2
+ """
3
+ Completes a list of variable names based on the provided partial string.
4
+
5
+ This function provides completion suggestions for variable names. If the
6
+ input string is empty, it returns a default list of variable names ordered
7
+ by their last updated timestamps. Otherwise, it performs a search based on
8
+ the provided input string to retrieve relevant variable names.
9
+
10
+ Args:
11
+ incomplete (str): The partial variable name string to search for. If an
12
+ empty string is provided, the function returns a list of the most
13
+ updated variable names.
14
+
15
+ Returns:
16
+ list[str]: A list of variable names that match the input string or
17
+ the most recently updated aliases if the input is empty.
18
+ """
19
+ from cmdbox.container import get_variable_services
20
+
21
+ svc = get_variable_services()
22
+ if incomplete == "":
23
+ variables = svc.list_variables(order_by="last_updated", limit=20)
24
+ else:
25
+ variables = svc.search(incomplete, fields="name", limit=20)
26
+ return [var.name for var in variables]
File without changes
@@ -0,0 +1,357 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Callable, Any, Sequence
4
+
5
+ import typer
6
+
7
+ from cmdbox.cli.common.update_fields import (
8
+ merge_fields,
9
+ parse_set_pairs,
10
+ filter_allowed,
11
+ )
12
+ from cmdbox.cli.handlers.common_handlers import get_tags_interactive
13
+ from cmdbox.cli.prompts.prompts import (
14
+ prompt_for_alias,
15
+ prompt_for_template,
16
+ prompt_for_description,
17
+ prompt_for_cwd,
18
+ prompt_for_shell,
19
+ prompt_for_timeout,
20
+ )
21
+
22
+ from cmdbox.cli.prompts.validators import AliasValidator, TemplateValidator
23
+ from cmdbox.cli.ui.console import ConsoleUI
24
+ from cmdbox.cli.ui.presenters.command_presenter import (
25
+ render_command,
26
+ render_command_list,
27
+ render_command_created,
28
+ render_command_updated,
29
+ render_command_deleted,
30
+ )
31
+ from cmdbox.cli.ui.presenters.result_presenter import (
32
+ render_tag_attach_result,
33
+ render_tag_detach_result,
34
+ )
35
+ from cmdbox.services.command_services import CommandServices
36
+ from cmdbox.services.field_selection import FieldSelectionResolver
37
+ from cmdbox.services.tag_services import TagServices
38
+ from cmdbox.settings.models import Settings
39
+ from cmdbox.logging_setup.log_decorators import log_action
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class AddCommandArgs:
44
+ alias: Optional[str]
45
+ template: Optional[str]
46
+ description: Optional[str]
47
+ tags: Optional[list[str]]
48
+ cwd: Optional[str] = None
49
+ shell: Optional[str] = None
50
+ env: Optional[list[str]] = None
51
+ timeout: Optional[int] = None
52
+ interactive: bool = False
53
+
54
+
55
+ @log_action(__name__, "run_add_command")
56
+ def run_add_command(
57
+ *,
58
+ args: AddCommandArgs,
59
+ get_cmd_services: Callable[[], CommandServices],
60
+ get_tag_services: Callable[[], TagServices],
61
+ get_console: Callable[[], ConsoleUI],
62
+ ) -> None:
63
+ alias = args.alias
64
+ template = args.template
65
+ description = args.description
66
+ tags = args.tags
67
+
68
+ if args.interactive or args.alias is None:
69
+ alias = prompt_for_alias(AliasValidator())
70
+
71
+ if args.interactive or args.template is None:
72
+ template = prompt_for_template(TemplateValidator())
73
+
74
+ if args.interactive or args.description is None:
75
+ description = prompt_for_description()
76
+
77
+ if args.interactive or args.tags is None:
78
+ tags = get_tags_interactive(get_tag_services())
79
+ if not tags:
80
+ tags = None
81
+
82
+ env = parse_env_pairs(args.env)
83
+
84
+ cmd_service = get_cmd_services()
85
+ cmd = cmd_service.create_command(
86
+ alias=alias,
87
+ template=template,
88
+ description=description,
89
+ tags=tags,
90
+ cwd=args.cwd,
91
+ shell=args.shell,
92
+ env=env,
93
+ timeout=args.timeout,
94
+ )
95
+ console = get_console()
96
+ console.print(render_command_created(cmd))
97
+
98
+
99
+ @log_action(__name__, "run_get_command")
100
+ def run_get_command(
101
+ *,
102
+ alias: str,
103
+ get_cmd_services: Callable[[], CommandServices],
104
+ get_console: Callable[[], ConsoleUI],
105
+ ) -> None:
106
+ console = get_console()
107
+ cmd_service = get_cmd_services()
108
+ cmd = cmd_service.get_command(alias)
109
+ rendered_cmd = render_command(cmd)
110
+ console.print(rendered_cmd)
111
+
112
+
113
+ @log_action(__name__, "run_update_command")
114
+ def run_update_command(
115
+ *,
116
+ alias: str,
117
+ template: Optional[str],
118
+ description: Optional[str],
119
+ new_alias: Optional[str],
120
+ cwd: Optional[str],
121
+ shell: Optional[str],
122
+ env: Optional[list[str]],
123
+ timeout: Optional[int],
124
+ set_pairs: Optional[Sequence[str]],
125
+ edit_mode: bool,
126
+ edit_fields: Optional[str],
127
+ get_cmd_services: Callable[[], CommandServices],
128
+ get_settings: Callable[[], Settings],
129
+ get_console: Callable[[], ConsoleUI],
130
+ ) -> None:
131
+ allowed = {"alias", "template", "description", "cwd", "shell", "env", "timeout"}
132
+ fields: dict[str, Any] = {}
133
+
134
+ cmd_service = get_cmd_services()
135
+ cmd = cmd_service.get_command(alias)
136
+ console = get_console()
137
+
138
+ if edit_mode:
139
+ if any([template, description, new_alias, cwd, shell, env, timeout, set_pairs]):
140
+ raise typer.BadParameter(
141
+ "--edit cannot be combined with field options or --set"
142
+ )
143
+
144
+ updated_fields: dict[str, Any] = {}
145
+ if edit_fields:
146
+ edit_fields = [x.strip() for x in edit_fields.split(",")]
147
+
148
+ field_aliases = get_settings().field_aliases.alias_mapping
149
+
150
+ def check_field_alias(field: str) -> bool:
151
+ return (
152
+ edit_fields is None
153
+ or field in edit_fields
154
+ or any(x in edit_fields for x in field_aliases.get(field, []))
155
+ )
156
+
157
+ if check_field_alias("alias"):
158
+ updated_fields["alias"] = prompt_for_alias(
159
+ AliasValidator(), default=cmd.alias
160
+ )
161
+ if check_field_alias("template"):
162
+ updated_fields["template"] = prompt_for_template(
163
+ TemplateValidator(), default=cmd.template
164
+ )
165
+ if check_field_alias("description"):
166
+ updated_fields["description"] = prompt_for_description(
167
+ default=cmd.description
168
+ )
169
+ if check_field_alias("cwd"):
170
+ updated_fields["cwd"] = prompt_for_cwd(default=cmd.cwd or "")
171
+ if check_field_alias("shell"):
172
+ updated_fields["shell"] = prompt_for_shell(default=cmd.shell or "") or None
173
+ if check_field_alias("timeout"):
174
+ updated_fields["timeout"] = prompt_for_timeout(
175
+ default=str(cmd.timeout) if cmd.timeout else ""
176
+ )
177
+
178
+ fields = updated_fields
179
+
180
+ else:
181
+ if template is not None:
182
+ fields["template"] = template
183
+ if description is not None:
184
+ fields["description"] = description
185
+ if new_alias is not None:
186
+ fields["alias"] = new_alias
187
+ if cwd is not None:
188
+ fields["cwd"] = cwd
189
+ if shell is not None:
190
+ fields["shell"] = shell
191
+ if env is not None:
192
+ fields["env"] = parse_env_pairs(env)
193
+ if timeout is not None:
194
+ fields["timeout"] = timeout
195
+
196
+ fields = merge_fields(fields, parse_set_pairs(set_pairs))
197
+ fields = filter_allowed(fields, allowed)
198
+
199
+ if not fields:
200
+ raise typer.BadParameter("No fields specified to update.")
201
+
202
+ stored_env = json.loads(cmd.env) if cmd.env else None
203
+ current = {
204
+ "alias": cmd.alias,
205
+ "template": cmd.template,
206
+ "description": cmd.description,
207
+ "cwd": cmd.cwd,
208
+ "shell": cmd.shell,
209
+ "env": stored_env,
210
+ "timeout": cmd.timeout,
211
+ }
212
+ fields = {key: value for key, value in fields.items() if current.get(key) != value}
213
+
214
+ if not fields:
215
+ console.info("No changes detected.")
216
+ return
217
+
218
+ cmd_service.update_command(alias, **fields)
219
+
220
+ updated_cmd = cmd_service.get_command_by_id(cmd.id)
221
+ console.print(render_command_updated(updated_cmd))
222
+
223
+
224
+ @log_action(__name__, "run_list_command")
225
+ def run_list_command(
226
+ *,
227
+ limit: int,
228
+ order: str,
229
+ tags: list[str],
230
+ fields: list[str] | None = None,
231
+ get_cmd_services: Callable[[], CommandServices],
232
+ get_settings: Callable[[], Settings],
233
+ get_console: Callable[[], ConsoleUI],
234
+ get_display_field_resolver: Callable[[], FieldSelectionResolver],
235
+ ) -> None:
236
+ console = get_console()
237
+ cmd_service = get_cmd_services()
238
+
239
+ settings = get_settings()
240
+ resolved_fields = get_display_field_resolver().resolve(
241
+ fields,
242
+ default_fields=settings.default_fields.command_output,
243
+ aliases=settings.field_aliases.alias_map,
244
+ )
245
+
246
+ cmds = cmd_service.list_commands(limit=limit, order_by=order, tags=tags)
247
+ rendered_cmd_list = render_command_list(
248
+ cmds, title="Commands", fields=resolved_fields
249
+ )
250
+ console.print(rendered_cmd_list)
251
+
252
+
253
+ @log_action(__name__, "run_search_command")
254
+ def run_search_command(
255
+ *,
256
+ term: str,
257
+ limit: int,
258
+ search_fields: list[str] | None = None,
259
+ fields: list[str] | None = None,
260
+ get_cmd_services: Callable[[], CommandServices],
261
+ get_settings: Callable[[], Settings],
262
+ get_console: Callable[[], ConsoleUI],
263
+ get_display_field_resolver: Callable[[], FieldSelectionResolver],
264
+ get_search_field_resolver: Callable[[], FieldSelectionResolver],
265
+ ) -> None:
266
+ console = get_console()
267
+ cmd_service = get_cmd_services()
268
+
269
+ settings = get_settings()
270
+ output_fields = get_display_field_resolver().resolve(
271
+ fields,
272
+ default_fields=settings.default_fields.command_output,
273
+ aliases=settings.field_aliases.alias_map,
274
+ )
275
+ search_fields = get_search_field_resolver().resolve(
276
+ search_fields,
277
+ default_fields=settings.default_fields.command_search,
278
+ aliases=settings.field_aliases.alias_map,
279
+ )
280
+
281
+ cmds = cmd_service.search(term, limit=limit, fields=search_fields)
282
+ rendered_cmd_list = render_command_list(
283
+ cmds, title="Search Results", fields=output_fields
284
+ )
285
+ console.print(rendered_cmd_list)
286
+
287
+
288
+ @log_action(__name__, "run_delete_command")
289
+ def run_delete_command(
290
+ *,
291
+ alias: str,
292
+ get_cmd_services: Callable[[], CommandServices],
293
+ get_console: Callable[[], ConsoleUI],
294
+ ) -> None:
295
+ console = get_console()
296
+ cmd_service = get_cmd_services()
297
+ cmd = cmd_service.get_command(alias)
298
+ if cmd_service.delete_command(alias):
299
+ console.print(render_command_deleted(cmd))
300
+ else:
301
+ console.error(f"Failed to delete command '{alias}'.")
302
+
303
+
304
+ @log_action(__name__, "run_attach_tags")
305
+ def run_attach_tags(
306
+ *,
307
+ alias: str | None = None,
308
+ tag_names: list[str] | None = None,
309
+ get_cmd_services: Callable[[], CommandServices],
310
+ get_tag_services: Callable[[], TagServices],
311
+ get_console: Callable[[], ConsoleUI],
312
+ ) -> None:
313
+ if alias is None:
314
+ alias = prompt_for_alias(AliasValidator())
315
+ if tag_names is None:
316
+ tag_names = get_tags_interactive(get_tag_services())
317
+ cmd_service = get_cmd_services()
318
+ result = cmd_service.add_tags(alias=alias, tags=tag_names)
319
+ console = get_console()
320
+ console.print(render_tag_attach_result(result))
321
+
322
+
323
+ @log_action(__name__, "run_detach_tags")
324
+ def run_detach_tags(
325
+ *,
326
+ alias: str | None = None,
327
+ tag_names: list[str] | None = None,
328
+ get_cmd_services: Callable[[], CommandServices],
329
+ get_tag_services: Callable[[], TagServices],
330
+ get_console: Callable[[], ConsoleUI],
331
+ ) -> None:
332
+ if alias is None:
333
+ alias = prompt_for_alias(AliasValidator())
334
+ if tag_names is None:
335
+ tag_names = get_tags_interactive(get_tag_services())
336
+ cmd_service = get_cmd_services()
337
+ result = cmd_service.remove_tags(alias=alias, tags=tag_names)
338
+ console = get_console()
339
+ console.print(render_tag_detach_result(result))
340
+
341
+
342
+ def parse_env_pairs(env: list[str] | None) -> dict[str, str] | None:
343
+ if not env:
344
+ return None
345
+ result = {}
346
+ for pair in env:
347
+ if "=" not in pair:
348
+ raise typer.BadParameter(
349
+ f"Invalid env format '{pair}'. Expected key=value."
350
+ )
351
+ key, _, value = pair.partition("=")
352
+ if not key:
353
+ raise typer.BadParameter(
354
+ f"Invalid env format '{pair}'. Key cannot be empty."
355
+ )
356
+ result[key] = value
357
+ return result or None
@@ -0,0 +1,15 @@
1
+ from cmdbox.cli.prompts.completers import TagCompleter
2
+ from cmdbox.cli.prompts.prompts import prompt_for_tags
3
+ from cmdbox.cli.prompts.validators import TagNameValidator
4
+ from cmdbox.services.tag_services import TagServices
5
+
6
+
7
+ def get_tags_interactive(tag_services: TagServices) -> list[str] | None:
8
+ def get_tags(query: str) -> list[str]:
9
+ found_tags = tag_services.search(query, fields="name")
10
+ return [tag.name for tag in found_tags]
11
+
12
+ tag_completer = TagCompleter(get_tags)
13
+ validator = TagNameValidator()
14
+ tags = prompt_for_tags(tag_completer, validator)
15
+ return tags
@@ -0,0 +1,94 @@
1
+ from typing import Callable
2
+
3
+ from cmdbox.cli.prompts.prompts import prompt_for_confirm
4
+ from cmdbox.cli.ui.console import ConsoleUI
5
+ from cmdbox.cli.ui.presenters.history_presenter import (
6
+ render_history_list,
7
+ render_history_entry,
8
+ render_history_cleared,
9
+ )
10
+ from cmdbox.logging_setup.log_decorators import log_action
11
+ from cmdbox.runtime.executor import RunContext
12
+ from cmdbox.services.history_service import HistoryService
13
+ from cmdbox.services.run_service import RunService
14
+
15
+
16
+ @log_action(__name__, "run_history_list")
17
+ def run_history_list(
18
+ *,
19
+ alias: str | None,
20
+ limit: int,
21
+ get_history_service: Callable[[], HistoryService],
22
+ get_console: Callable[[], ConsoleUI],
23
+ ) -> None:
24
+ service = get_history_service()
25
+ entries = service.get_recent(alias=alias, limit=limit)
26
+ console = get_console()
27
+ if not entries:
28
+ console.info("No history found")
29
+ return
30
+ console.print(render_history_list(entries))
31
+
32
+
33
+ @log_action(__name__, "run_history_show")
34
+ def run_history_show(
35
+ *,
36
+ ref: str,
37
+ get_history_service: Callable[[], HistoryService],
38
+ get_console: Callable[[], ConsoleUI],
39
+ ) -> None:
40
+ service = get_history_service()
41
+ entry = service.get_by_ref(ref)
42
+ variables = service.get_variables(entry)
43
+ console = get_console()
44
+ console.print(render_history_entry(entry, variables))
45
+
46
+
47
+ @log_action(__name__, "run_history_rerun")
48
+ def run_history_rerun(
49
+ *,
50
+ ref: str,
51
+ get_history_service: Callable[[], HistoryService],
52
+ get_run_service: Callable[[], RunService],
53
+ ) -> None:
54
+ history_service = get_history_service()
55
+ entry = history_service.get_by_ref(ref)
56
+ variables = history_service.get_variables(entry)
57
+ run_service = get_run_service()
58
+ run_service.run(entry.alias, ctx=RunContext(), runtime_vars=variables)
59
+
60
+
61
+ @log_action(__name__, "run_history_clear")
62
+ def run_history_clear(
63
+ *,
64
+ alias: str | None,
65
+ yes: bool,
66
+ get_history_service: Callable[[], HistoryService],
67
+ get_console: Callable[[], ConsoleUI],
68
+ ) -> None:
69
+ console = get_console()
70
+ if not yes:
71
+ scope = f" for '{alias}'" if alias else ""
72
+ if not prompt_for_confirm(f"Clear all history{scope}?"):
73
+ console.info("Aborted")
74
+ return
75
+ service = get_history_service()
76
+ count = service.clear(alias=alias)
77
+ console.print(render_history_cleared(count, alias))
78
+
79
+
80
+ @log_action(__name__, "run_return_last")
81
+ def run_rerun_last(
82
+ *,
83
+ get_history_service: Callable[[], HistoryService],
84
+ get_run_service: Callable[[], RunService],
85
+ get_console: Callable[[], ConsoleUI],
86
+ ) -> None:
87
+ service = get_history_service()
88
+ entries = service.get_recent(limit=1)
89
+ if not entries:
90
+ get_console().info("No command history found")
91
+ return
92
+ entry = entries[0]
93
+ variables = service.get_variables(entry)
94
+ get_run_service().run(entry.alias, runtime_vars=variables)