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,127 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from typing import Callable
4
+
5
+ import typer
6
+
7
+ from cmdbox.cli.ui.console import ConsoleUI
8
+ from cmdbox.cli.ui.presenters.init_presenter import (
9
+ render_install_instructions,
10
+ render_install_success,
11
+ render_shell_output,
12
+ )
13
+ from cmdbox.init.detect import detect_shell
14
+ from cmdbox.init.io import load_integration_text, upsert_marked_block
15
+ from cmdbox.init.specs import SHELLS
16
+ from cmdbox.logging_setup.log_decorators import log_action
17
+
18
+
19
+ @log_action(__name__, "run_init_command")
20
+ def run_init_command(
21
+ *,
22
+ shell: str = None,
23
+ install: bool = False,
24
+ path: str = None,
25
+ get_console: Callable[[], ConsoleUI],
26
+ ) -> None:
27
+ """
28
+ Executes the initialization command for shell integration, handling snippet output,
29
+ installation, and providing specific configurations based on the operating system shell.
30
+
31
+ This function manages different installation modes (e.g., updating profile files,
32
+ writing integration files, or providing wrapper hints) and facilitates smooth integration
33
+ of the application with the user's shell environment.
34
+
35
+ Args:
36
+ shell (str): The shell name provided for integration. Must match one of the expected keys
37
+ in the `SHELLS` dictionary (case-insensitive).
38
+ install (bool): Specifies whether to install the integration snippet to the shell's
39
+ configuration file or output the snippet to stdout. Defaults to `False`.
40
+ path (str, optional): A custom path to the file to which the integration snippet
41
+ will be written when `install` is `True`. If not provided, defaults to the
42
+ path determined by the shell specification.
43
+ get_console (Callable[[], ConsoleUI]): A callable returning an instance of `ConsoleUI`
44
+ to handle console output and user feedback.
45
+
46
+ Raises:
47
+ typer.BadParameter: If an invalid shell name is provided that does not exist in the
48
+ `SHELLS` dictionary.
49
+ """
50
+ console = get_console()
51
+
52
+ if not shell:
53
+ shell = detect_shell()
54
+
55
+ shell_key = shell.lower()
56
+ if shell_key not in SHELLS:
57
+ raise typer.BadParameter(f"Invalid shell: {shell}")
58
+
59
+ spec = SHELLS[shell_key]
60
+ snippet = load_integration_text(spec.filename)
61
+
62
+ if not install:
63
+ title = f"Install instructions for {spec.name}"
64
+ console.print(
65
+ render_install_instructions(
66
+ snippet, shell=shell, title=title, include_help_text=shell != "cmd"
67
+ )
68
+ )
69
+ return
70
+
71
+ target = (
72
+ Path(path)
73
+ if path
74
+ else (spec.default_path_fn() if spec.default_path_fn else None)
75
+ )
76
+
77
+ if spec.install_mode == "profile_block":
78
+ if target is None:
79
+ raise typer.BadParameter(
80
+ f"No default install path available for shell: {spec.name}."
81
+ )
82
+ upsert_marked_block(target, snippet)
83
+ console.print(render_install_success())
84
+ return
85
+
86
+ if spec.install_mode == "write_file":
87
+ if target is None:
88
+ raise typer.BadParameter(
89
+ f"No default install path available for shell: {spec.name}."
90
+ )
91
+
92
+ if target.exists():
93
+ backup = target.with_suffix(target.suffix + ".bak")
94
+ shutil.copy2(target, backup)
95
+
96
+ target.parent.mkdir(parents=True, exist_ok=True)
97
+ target.write_text(snippet, encoding="utf-8")
98
+ console.print(render_install_success())
99
+ return
100
+
101
+ if spec.install_mode == "wrapper_hint":
102
+ title = "Unable to install snippet for cmd shell"
103
+ console.print(
104
+ render_install_instructions(
105
+ snippet, shell="cmd", title=title, include_help_text=False
106
+ )
107
+ )
108
+ return
109
+
110
+
111
+ @log_action(__name__, "run_detect_shell")
112
+ def run_detect_shell(*, get_console: Callable[[], ConsoleUI]) -> None:
113
+ """
114
+ Detects the current shell being used and prints it to the console.
115
+
116
+ This function retrieves a `ConsoleUI` object using the provided `get_console`
117
+ callable. It then detects the current shell and sends a message with the name
118
+ of the detected shell to be displayed on the console.
119
+
120
+ Args:
121
+ get_console (Callable[[], ConsoleUI]): A callable that, when invoked,
122
+ returns an instance of a `ConsoleUI` object used for displaying the
123
+ detected shell information.
124
+ """
125
+ console = get_console()
126
+ shell = detect_shell()
127
+ console.print(render_shell_output(shell))
@@ -0,0 +1,178 @@
1
+ import logging
2
+ from typing import Callable
3
+ from dataclasses import dataclass
4
+
5
+ import typer
6
+
7
+ from cmdbox.cli.prompts.prompts import prompt_for_missing_var
8
+ from cmdbox.cli.ui.console import ConsoleUI
9
+ from cmdbox.cli.ui.presenters.result_presenter import (
10
+ render_execution_result,
11
+ render_preview_result,
12
+ )
13
+ from cmdbox.runtime.executor import RunContext
14
+ from cmdbox.services.run_service import RunService
15
+ from cmdbox.settings.models import Settings
16
+ from cmdbox.logging_setup.log_decorators import log_action
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass(frozen=False)
22
+ class RawRunContext:
23
+ """
24
+ This dataclass is effectively the same as the actual RunContext
25
+ except that it takes a different 'pre-parsed' version of the
26
+ env argument. This class takes the argument in a format that
27
+ can be supplied by the user, which must then be parsed into a
28
+ format that can be used by the RunContext.
29
+ """
30
+
31
+ cwd: str | None = None
32
+ env: list[str] | str | None = None
33
+ capture: bool = False
34
+ shell: str | None = None
35
+ timeout: int | None = None
36
+ emit: bool = False
37
+ verbose: bool = False
38
+
39
+
40
+ @log_action(__name__, "run_run_command")
41
+ def run_run_command(
42
+ *,
43
+ alias: str,
44
+ runtime_vars: dict[str, str] | None = None,
45
+ run_ctx: RawRunContext | None = None,
46
+ get_run_service: Callable[[], RunService],
47
+ get_settings: Callable[[], Settings],
48
+ get_console: Callable[[], ConsoleUI],
49
+ ):
50
+ if run_ctx and run_ctx.verbose is None:
51
+ settings = get_settings()
52
+ run_ctx.verbose = settings.execution_settings.default_verbose
53
+
54
+ run_service = get_run_service()
55
+
56
+ missing = run_service.collect_missing_vars(alias, runtime_vars=runtime_vars)
57
+ if missing:
58
+ for var_name in missing:
59
+ value = prompt_for_missing_var(var_name)
60
+ runtime_vars[var_name] = value
61
+
62
+ run_ctx = get_run_ctx(run_ctx) if run_ctx else RunContext()
63
+ ex_result = run_service.run(alias, ctx=run_ctx, runtime_vars=runtime_vars)
64
+ if run_ctx.verbose and not run_ctx.emit:
65
+ console = get_console()
66
+ console.print(render_execution_result(ex_result))
67
+
68
+
69
+ @log_action(__name__, "run_preview_command")
70
+ def run_preview_command(
71
+ *,
72
+ alias: str,
73
+ runtime_vars: dict[str, str] | None = None,
74
+ run_ctx: RawRunContext | None = None,
75
+ get_run_service: Callable[[], RunService],
76
+ get_settings: Callable[[], Settings],
77
+ get_console: Callable[[], ConsoleUI],
78
+ ):
79
+ if run_ctx and run_ctx.verbose is None:
80
+ settings = get_settings()
81
+ run_ctx.verbose = settings.execution_settings.default_verbose
82
+
83
+ run_service = get_run_service()
84
+
85
+ missing = run_service.collect_missing_vars(alias, runtime_vars=runtime_vars)
86
+ if missing:
87
+ for var_name in missing:
88
+ value = prompt_for_missing_var(var_name)
89
+ runtime_vars[var_name] = value
90
+
91
+ run_ctx = get_run_ctx(run_ctx) if run_ctx else RunContext()
92
+ prev_result, effective_ctx = run_service.preview(
93
+ alias, runtime_vars=runtime_vars, ctx=run_ctx
94
+ )
95
+ rendered_result = render_preview_result(prev_result, ctx=effective_ctx)
96
+ console = get_console()
97
+ console.print(rendered_result)
98
+
99
+
100
+ def get_run_ctx(raw_run_ctx: RawRunContext | None) -> RunContext:
101
+ """
102
+ Creates and returns a `RunContext` object based on the provided `raw_run_ctx`.
103
+
104
+ If the `raw_run_ctx` is not provided (i.e., is None), a default `RunContext` is
105
+ created and returned. If `raw_run_ctx` is provided, its properties are used to
106
+ initialize the `RunContext`, and its environment is parsed before being passed
107
+ to the new context.
108
+
109
+ Args:
110
+ raw_run_ctx: An instance of `RawRunContext` or None. If provided, it must
111
+ contain context information such as current working directory, shell
112
+ configuration, and environment variables.
113
+
114
+ Returns:
115
+ RunContext: A `RunContext` object constructed using the provided
116
+ `raw_run_ctx` or with default values if no `raw_run_ctx` is provided.
117
+ """
118
+ if raw_run_ctx is None:
119
+ return RunContext()
120
+
121
+ env = parse_env(raw_run_ctx.env)
122
+ return RunContext(
123
+ cwd=raw_run_ctx.cwd,
124
+ env=env,
125
+ capture=raw_run_ctx.capture,
126
+ shell=raw_run_ctx.shell,
127
+ timeout=raw_run_ctx.timeout,
128
+ emit=raw_run_ctx.emit,
129
+ verbose=raw_run_ctx.verbose,
130
+ )
131
+
132
+
133
+ def parse_env(env: list[str] | str | None) -> dict[str, str] | None:
134
+ """
135
+ Parses a list, string, or None value into a dictionary where each key-value pair
136
+ represents an environment variable, with the key and value extracted from the
137
+ input format. If the input is None, returns None.
138
+
139
+ Args:
140
+ env (list[str] | str | None): A list of strings, a comma-separated string,
141
+ or None, where each string is in the format "key=value".
142
+
143
+ Returns:
144
+ dict[str, str] | None: A dictionary containing the parsed environment
145
+ variables as key-value pairs, or None if the input is None.
146
+
147
+ Raises:
148
+ typer.BadParameter: If an entry in the input does not follow the
149
+ "key=value" format.
150
+ """
151
+ if env is None:
152
+ return None
153
+
154
+ ret = {}
155
+
156
+ if isinstance(env, str):
157
+ env = env.split(",")
158
+
159
+ def split_pair(pair: str) -> tuple[str, str]:
160
+ if "=" not in pair:
161
+ log.error(f"Invalid environment variable format: {pair}")
162
+ raise typer.BadParameter(
163
+ "Invalid environment variable format. Each env must be in the format of key=value."
164
+ )
165
+ k, v = pair.split("=", maxsplit=1)
166
+ log.debug(f"Parsed env: {k}={v}")
167
+ return k, v
168
+
169
+ for pair in env:
170
+ if "," in pair:
171
+ for k, v in map(split_pair, pair.split(",")):
172
+ ret[k] = v
173
+ else:
174
+ k, v = split_pair(pair)
175
+ ret[k] = v
176
+
177
+ log.debug(f"Parsed env: {ret}")
178
+ return ret
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from typing import Callable
3
+
4
+ import typer
5
+
6
+ from cmdbox.cli.ui.console import ConsoleUI
7
+ from cmdbox.cli.ui.editor import edit_text_fullscreen, EditCanceled, edit_text_in_editor
8
+ from cmdbox.cli.ui.presenters.settings_presenter import render_settings_show
9
+ from cmdbox.logging_setup.log_decorators import log_action
10
+ from cmdbox.settings.settings_service import SettingsService
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ @log_action(__name__, "run_edit_settings")
16
+ def run_edit_settings(
17
+ external: bool = True,
18
+ *,
19
+ get_settings_service: Callable[[], SettingsService],
20
+ get_console: Callable[[], ConsoleUI],
21
+ ):
22
+ settings_service = get_settings_service()
23
+ console = get_console()
24
+
25
+ def editor_fn(initial_text: str) -> str:
26
+ if external:
27
+ return edit_text_in_editor(
28
+ initial_text, suffix=".toml", title_hint="CmdBox Settings"
29
+ )
30
+ else:
31
+ return edit_text_fullscreen(initial_text, title="Edit Settings")
32
+
33
+ try:
34
+ settings_service.edit(editor_fn)
35
+ console.success("Settings saved.")
36
+ except EditCanceled:
37
+ console.info("Settings not saved.")
38
+ raise typer.Exit(code=0)
39
+ except Exception as exc:
40
+ log.error("Error saving settings.", exc_info=True)
41
+ console.error(f"Error saving settings: {exc}")
42
+ raise typer.Exit(code=1)
43
+
44
+
45
+ @log_action(__name__, "run_show_settings")
46
+ def run_show_settings(
47
+ fields: str | None = None,
48
+ *,
49
+ get_settings_service: Callable[[], SettingsService],
50
+ get_console: Callable[[], ConsoleUI],
51
+ ):
52
+ parsed_fields = fields.split(",") if fields else None
53
+
54
+ settings_service = get_settings_service()
55
+ console = get_console()
56
+
57
+ settings = settings_service.get()
58
+ output = render_settings_show(settings, parsed_fields)
59
+ console.print(output)
@@ -0,0 +1,220 @@
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.prompts.prompts import (
12
+ prompt_for_name,
13
+ prompt_for_description,
14
+ )
15
+ from cmdbox.cli.ui.presenters.tag_presenter import (
16
+ render_tag_created,
17
+ render_tag,
18
+ render_tag_list,
19
+ render_tag_updated,
20
+ render_tag_deleted,
21
+ )
22
+ from cmdbox.cli.prompts.validators import TagNameValidator
23
+ from cmdbox.cli.ui.console import ConsoleUI
24
+ from cmdbox.services.field_selection import FieldSelectionResolver
25
+ from cmdbox.services.tag_services import TagServices
26
+ from cmdbox.settings.models import Settings
27
+ from cmdbox.logging_setup.log_decorators import log_action
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class AddTagArgs:
32
+ name: Optional[str]
33
+ description: Optional[str]
34
+ interactive: bool = False
35
+
36
+
37
+ @log_action(__name__, "run_add_tag")
38
+ def run_add_tag(
39
+ *,
40
+ args: AddTagArgs,
41
+ get_tag_services: Callable[[], TagServices],
42
+ get_console: Callable[[], ConsoleUI],
43
+ ) -> None:
44
+ name = args.name
45
+ description = args.description
46
+
47
+ if args.interactive or name is None:
48
+ name = prompt_for_name(TagNameValidator())
49
+
50
+ if args.interactive or description is None:
51
+ description = prompt_for_description()
52
+
53
+ tag_service = get_tag_services()
54
+ tag = tag_service.create_tag(name=name, description=description)
55
+ console = get_console()
56
+ console.print(render_tag_created(tag))
57
+
58
+
59
+ @log_action(__name__, "run_get_tag")
60
+ def run_get_tag(
61
+ *,
62
+ name: str,
63
+ get_tag_services: Callable[[], TagServices],
64
+ get_console: Callable[[], ConsoleUI],
65
+ ) -> None:
66
+ console = get_console()
67
+ tag_service = get_tag_services()
68
+ tag = tag_service.get_tag(name)
69
+ console.print(render_tag(tag))
70
+
71
+
72
+ @log_action(__name__, "run_update_tag")
73
+ def run_update_tag(
74
+ *,
75
+ name: str,
76
+ description: Optional[str],
77
+ new_name: Optional[str],
78
+ set_pairs: Optional[Sequence[str]],
79
+ edit_mode: bool,
80
+ edit_fields: Optional[str],
81
+ get_tag_services: Callable[[], TagServices],
82
+ get_settings: Callable[[], Settings],
83
+ get_console: Callable[[], ConsoleUI],
84
+ ) -> None:
85
+ allowed = {"name", "description"}
86
+ fields: Dict[str, Any] = {}
87
+
88
+ tag_service = get_tag_services()
89
+ tag = tag_service.get_tag(name)
90
+ console = get_console()
91
+
92
+ if edit_mode:
93
+ if any([description, new_name, set_pairs]):
94
+ raise typer.BadParameter(
95
+ "--edit cannot be combined with field options or --set."
96
+ )
97
+
98
+ updated_fields: dict[str, Any] = {}
99
+ if edit_fields:
100
+ edit_fields = [x.strip() for x in edit_fields.split(",")]
101
+
102
+ field_aliases = get_settings().field_aliases.alias_mapping
103
+
104
+ def check_field_aliases(field: str) -> bool:
105
+ return (
106
+ edit_fields is None
107
+ or field in edit_fields
108
+ or any(x in edit_fields for x in field_aliases.get(field, []))
109
+ )
110
+
111
+ if check_field_aliases("name"):
112
+ updated_fields["name"] = prompt_for_name(
113
+ TagNameValidator(), default=tag.name
114
+ )
115
+ if check_field_aliases("description"):
116
+ updated_fields["description"] = prompt_for_description(
117
+ default=tag.description
118
+ )
119
+
120
+ fields = updated_fields
121
+
122
+ else:
123
+ if description is not None:
124
+ fields["description"] = description
125
+ if new_name is not None:
126
+ fields["name"] = new_name
127
+
128
+ fields = merge_fields(fields, parse_set_pairs(set_pairs))
129
+ fields = filter_allowed(fields, allowed)
130
+
131
+ if not fields:
132
+ raise typer.BadParameter("No fields specified to update.")
133
+
134
+ current = {
135
+ "name": tag.name,
136
+ "description": tag.description,
137
+ }
138
+ fields = {key: value for key, value in fields.items() if current.get(key) != value}
139
+
140
+ if not fields:
141
+ console.info("No changes detected.")
142
+ return
143
+
144
+ tag_service.update_tag(name, **fields)
145
+
146
+ updated_tag = tag_service.get_tag_by_id(tag.id)
147
+ console.print(render_tag_updated(updated_tag))
148
+
149
+
150
+ @log_action(__name__, "run_list_tags")
151
+ def run_list_tags(
152
+ *,
153
+ limit: int,
154
+ order_by: str,
155
+ fields: list[str] | None = None,
156
+ get_tag_services: Callable[[], TagServices],
157
+ get_settings: Callable[[], Settings],
158
+ get_console: Callable[[], ConsoleUI],
159
+ get_display_field_resolver: Callable[[], FieldSelectionResolver],
160
+ ) -> None:
161
+ console = get_console()
162
+ tag_service = get_tag_services()
163
+ tags = tag_service.list_tags(limit=limit, order_by=order_by)
164
+
165
+ settings = get_settings()
166
+ fields = get_display_field_resolver().resolve(
167
+ fields,
168
+ default_fields=settings.default_fields.tag_output,
169
+ aliases=settings.field_aliases.alias_map,
170
+ )
171
+
172
+ console.print(render_tag_list(tags, title="Tags", fields=fields))
173
+
174
+
175
+ @log_action(__name__, "run_search_tags")
176
+ def run_search_tags(
177
+ *,
178
+ term: str,
179
+ limit: int,
180
+ search_fields: list[str] | None = None,
181
+ fields: list[str] | None = None,
182
+ get_tag_services: Callable[[], TagServices],
183
+ get_settings: Callable[[], Settings],
184
+ get_console: Callable[[], ConsoleUI],
185
+ get_display_field_resolver: Callable[[], FieldSelectionResolver],
186
+ get_search_field_resolver: Callable[[], FieldSelectionResolver],
187
+ ) -> None:
188
+ console = get_console()
189
+ tag_service = get_tag_services()
190
+
191
+ settings = get_settings()
192
+ output_fields = get_display_field_resolver().resolve(
193
+ fields,
194
+ default_fields=settings.default_fields.tag_output,
195
+ aliases=settings.field_aliases.alias_map,
196
+ )
197
+ search_fields = get_search_field_resolver().resolve(
198
+ search_fields,
199
+ default_fields=settings.default_fields.tag_search,
200
+ aliases=settings.field_aliases.alias_map,
201
+ )
202
+
203
+ tags = tag_service.search(term, limit=limit, fields=search_fields)
204
+ console.print(render_tag_list(tags, title="Search Results", fields=output_fields))
205
+
206
+
207
+ @log_action(__name__, "run_delete_tag")
208
+ def run_delete_tag(
209
+ *,
210
+ name: str,
211
+ get_tag_services: Callable[[], TagServices],
212
+ get_console: Callable[[], ConsoleUI],
213
+ ) -> None:
214
+ console = get_console()
215
+ tag_service = get_tag_services()
216
+ tag = tag_service.get_tag(name)
217
+ if tag_service.delete_tag(name):
218
+ console.print(render_tag_deleted(tag))
219
+ else:
220
+ console.error(f"Failed to delete tag '{name}'.")