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,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)
|