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,141 @@
1
+ import logging
2
+ import os
3
+ import shlex
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ from prompt_toolkit import Application
10
+ from prompt_toolkit.key_binding import KeyBindings
11
+ from prompt_toolkit.layout import Layout
12
+ from prompt_toolkit.styles import Style
13
+ from prompt_toolkit.widgets import TextArea, Frame
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class EditCanceled(Exception):
19
+ pass
20
+
21
+
22
+ def edit_text_fullscreen(initial_text: str, title: str = "Edit") -> str:
23
+ """
24
+ Opens a fullscreen text editor in the terminal for the user to edit the
25
+ provided text.
26
+
27
+ This function creates an interactive fullscreen text editor with options for
28
+ saving or canceling edits. The user can press `Ctrl+S` to save changes,
29
+ `Ctrl+Q` or `Esc` to cancel editing, and directly work with a text area that
30
+ supports multiline editing, scrolling, and line numbers.
31
+
32
+ Args:
33
+ initial_text (str): The initial text content to be edited by the user.
34
+ title (str, optional): The title of the editor window. Defaults to "Edit".
35
+
36
+ Returns:
37
+ str: The edited text after the user saves and exits the editor.
38
+ """
39
+ log.info(
40
+ "Editing text in fullscreen editor for '%s'. text_length=%s",
41
+ title,
42
+ len(initial_text),
43
+ )
44
+ kb = KeyBindings()
45
+
46
+ text_area = TextArea(
47
+ text=initial_text,
48
+ multiline=True,
49
+ scrollbar=True,
50
+ line_numbers=True,
51
+ wrap_lines=False,
52
+ )
53
+
54
+ @kb.add("c-s")
55
+ def _save(event):
56
+ event.app.exit(result=text_area.text)
57
+
58
+ @kb.add("c-q")
59
+ @kb.add("escape")
60
+ def _cancel(event):
61
+ event.app.exit(exception=EditCanceled())
62
+
63
+ root = Frame(text_area, title=f"{title} (Ctrl+S to save, Esc to cancel)")
64
+ app = Application(
65
+ layout=Layout(root),
66
+ key_bindings=kb,
67
+ full_screen=True,
68
+ mouse_support=True,
69
+ style=Style.from_dict({}),
70
+ )
71
+
72
+ return app.run()
73
+
74
+
75
+ def edit_text_in_editor(
76
+ initial_text: str, suffix: str = ".txt", title_hint: str | None = None
77
+ ) -> str:
78
+ """
79
+ Opens the given initial text in a temporary file for editing using a text editor
80
+ resolved by the system. The final text, after editing, is returned as a string.
81
+
82
+ Args:
83
+ initial_text: The initial text content to populate the temporary file with.
84
+ suffix: The file suffix/extension to use for the temporary file. Defaults to ".txt".
85
+ title_hint: Optional hint for the editor window title.
86
+
87
+ Returns:
88
+ Edited content as a string after modifications in the editor.
89
+
90
+ Raises:
91
+ RuntimeError: If the editor cannot be found or launched properly.
92
+ """
93
+ log.info("Opening text editor for '%s'", title_hint)
94
+ editor_cmd = resolve_editor()
95
+ log.debug("Using editor: %s", editor_cmd)
96
+
97
+ with tempfile.TemporaryDirectory(prefix="cb_edit_") as td:
98
+ path = Path(td) / f"edit{suffix}"
99
+ path.write_text(initial_text, encoding="utf-8")
100
+ cmd = [*editor_cmd, str(path)]
101
+
102
+ env = os.environ.copy()
103
+ if title_hint:
104
+ env["CB_EDIT_TITLE"] = title_hint
105
+
106
+ try:
107
+ subprocess.run(cmd, check=False, env=env)
108
+ except FileNotFoundError as e:
109
+ log.error("Unable to find editor %s", editor_cmd)
110
+ raise RuntimeError(f"Unable to find editor: {editor_cmd}") from e
111
+
112
+ return path.read_text(encoding="utf-8")
113
+
114
+
115
+ def resolve_editor() -> list[str]:
116
+ """
117
+ Determines the command used to open a text editor based on user environment
118
+ or available system defaults.
119
+
120
+ If an editor is specified in the `VISUAL` or `EDITOR` environment variables,
121
+ this value is used. Otherwise, the function falls back to platform-specific
122
+ default editors or searches for common command-line editors available on the
123
+ system.
124
+
125
+ Returns:
126
+ list[str]: A list representing the command to open a text editor.
127
+ For example, it may contain the editor's executable name or path, along
128
+ with any required arguments.
129
+ """
130
+ editor = os.environ.get("VISUAL") or os.environ.get("EDITOR")
131
+ if editor:
132
+ return shlex.split(editor)
133
+
134
+ if os.name == "nt":
135
+ return ["notepad"]
136
+
137
+ for candidate in ["nano", "vim", "vi"]:
138
+ if shutil.which(candidate) is not None:
139
+ return [candidate]
140
+
141
+ return ["vi"]
File without changes
@@ -0,0 +1,8 @@
1
+ from cmdbox.cli.ui.primitives import banner
2
+
3
+
4
+ def render_version(version: str):
5
+ return banner(
6
+ f"CmdBox {version}",
7
+ status="info",
8
+ )
@@ -0,0 +1,168 @@
1
+ import json
2
+ from typing import Sequence, Callable
3
+
4
+ from cmdbox.cli.ui.primitives import (
5
+ col,
6
+ pluralize,
7
+ table_panel,
8
+ kv_table,
9
+ section,
10
+ tag_block,
11
+ )
12
+ from cmdbox.models import Command
13
+
14
+ COMMAND_COLUMNS: dict[str, tuple[str, dict, Callable[[Command], object]]] = {
15
+ "alias": (
16
+ "Alias",
17
+ {"style": "entity.name", "no_wrap": True},
18
+ lambda c: c.alias,
19
+ ),
20
+ "template": (
21
+ "Template",
22
+ {"style": "code.inline", "overflow": "fold"},
23
+ lambda c: c.template,
24
+ ),
25
+ "description": (
26
+ "Description",
27
+ {"overflow": "fold"},
28
+ lambda c: c.description,
29
+ ),
30
+ "cwd": (
31
+ "Working Directory",
32
+ {"overflow": "fold"},
33
+ lambda c: c.cwd,
34
+ ),
35
+ "shell": (
36
+ "Shell",
37
+ {"no_wrap": True},
38
+ lambda c: c.shell,
39
+ ),
40
+ "env": (
41
+ "Environment",
42
+ {"overflow": "fold"},
43
+ lambda c: format_env(c.env),
44
+ ),
45
+ "timeout": (
46
+ "Timeout",
47
+ {"no_wrap": True},
48
+ lambda c: f"{c.timeout}s" if c.timeout is not None else None,
49
+ ),
50
+ "used": (
51
+ "Used",
52
+ {"style": "entity.count", "no_wrap": True, "justify": "right"},
53
+ lambda c: c.used,
54
+ ),
55
+ "last_used": (
56
+ "Last Used",
57
+ {"style": "entity.time", "no_wrap": True},
58
+ lambda c: c.last_used,
59
+ ),
60
+ "date_created": (
61
+ "Created",
62
+ {"style": "entity.time", "no_wrap": True},
63
+ lambda c: c.date_created,
64
+ ),
65
+ "last_updated": (
66
+ "Updated",
67
+ {"style": "entity.time", "no_wrap": True},
68
+ lambda c: c.last_updated,
69
+ ),
70
+ "tags": (
71
+ "Tags",
72
+ {"overflow": "fold"},
73
+ lambda c: tag_block([x.tag.name for x in c.tags]),
74
+ ),
75
+ }
76
+
77
+ DEFAULT_FIELDS = ["alias", "template", "description"]
78
+
79
+
80
+ def render_command_created(command: Command):
81
+ rendered_command = render_command(command)
82
+ return section(
83
+ title=f"Command '{command.alias}' created",
84
+ body=rendered_command,
85
+ border_style="status.success",
86
+ )
87
+
88
+
89
+ def render_command(command: Command):
90
+ # Conditional fields will only be shown if they are not None
91
+ conditional_fields = ["cwd", "shell", "env", "timeout"]
92
+ rows = []
93
+ for key, value in COMMAND_COLUMNS.items():
94
+ header, _, extractor = value
95
+ extracted_value = extractor(command)
96
+ if key in conditional_fields and extracted_value is None:
97
+ continue
98
+ rows.append((header, extracted_value))
99
+ cmd_display = kv_table(rows)
100
+ return cmd_display
101
+
102
+
103
+ def render_command_list(
104
+ commands: Sequence[Command], *, title: str = None, fields: list[str] | None = None
105
+ ):
106
+ active_fields = fields or DEFAULT_FIELDS
107
+ active_fields = [f for f in active_fields if f in COMMAND_COLUMNS]
108
+
109
+ columns = []
110
+ extractors = []
111
+
112
+ for field in active_fields:
113
+ header, col_args, extractor = COMMAND_COLUMNS[field]
114
+ columns.append(col(header, **col_args))
115
+ extractors.append(extractor)
116
+
117
+ rows = [tuple(extractor(c) for extractor in extractors) for c in commands]
118
+
119
+ caption = f"{pluralize(len(commands), 'command')} found"
120
+
121
+ return table_panel(
122
+ title=title or "Commands",
123
+ columns=columns,
124
+ rows=rows,
125
+ caption=caption,
126
+ )
127
+
128
+
129
+ def render_command_updated(command: Command):
130
+ rendered_command = render_command(command)
131
+ return section(
132
+ title=f"Command '{command.alias}' updated",
133
+ body=rendered_command,
134
+ border_style="status.success",
135
+ )
136
+
137
+
138
+ def render_command_deleted(command: Command):
139
+ rendered_command = render_command(command)
140
+ return section(
141
+ title=f"Command '{command.alias}' deleted",
142
+ body=rendered_command,
143
+ border_style="status.success",
144
+ )
145
+
146
+
147
+ def format_env(env: str | None) -> str | None:
148
+ """
149
+ Formats the provided environment string by parsing it as JSON and converting the
150
+ key-value pairs into a newline-separated string. If the string is not valid JSON
151
+ or parsing fails, the original string is returned.
152
+
153
+ Args:
154
+ env (str | None): A string representing the environment in JSON format, or
155
+ None if no environment is provided.
156
+
157
+ Returns:
158
+ str | None: A formatted string with key-value pairs separated by newlines
159
+ if the input was parsed successfully; otherwise, the original string or
160
+ None if the input was None.
161
+ """
162
+ if not env:
163
+ return None
164
+ try:
165
+ parsed = json.loads(env)
166
+ return "\n".join(f"{k}={v}" for k, v in parsed.items())
167
+ except (json.JSONDecodeError, AttributeError):
168
+ return env
@@ -0,0 +1,83 @@
1
+ from rich.text import Text
2
+
3
+ from cmdbox.cli.ui.primitives import (
4
+ to_text,
5
+ col,
6
+ table_panel,
7
+ pluralize,
8
+ kv_table,
9
+ section,
10
+ status_line,
11
+ )
12
+ from cmdbox.models import CommandHistory
13
+
14
+
15
+ def _short_id(entry: CommandHistory) -> str:
16
+ return entry.id[:6]
17
+
18
+
19
+ def _format_exit(exit_code: int | None) -> Text:
20
+ if exit_code is None:
21
+ return to_text("—", style="ui.muted")
22
+ if exit_code == 0:
23
+ return to_text("0", style="status.success")
24
+ return to_text(str(exit_code), style="status.error")
25
+
26
+
27
+ HISTORY_LIST_COLUMNS = [
28
+ col("#", style="ui.muted", justify="right", no_wrap=True),
29
+ col("ID", style="entity.id", no_wrap=True),
30
+ col("Alias", style="entity.name", no_wrap=True),
31
+ col("Ran At", style="entity.time", no_wrap=True),
32
+ col("Exit", justify="center", no_wrap=True),
33
+ ]
34
+
35
+
36
+ def _list_rows(entries: list[CommandHistory]) -> list[tuple]:
37
+ return [
38
+ (
39
+ str(i),
40
+ _short_id(entry),
41
+ entry.alias,
42
+ to_text(entry.ran_at),
43
+ _format_exit(entry.exit_code),
44
+ )
45
+ for i, entry in enumerate(entries, start=1)
46
+ ]
47
+
48
+
49
+ def render_history_list(entries: list[CommandHistory]):
50
+ return table_panel(
51
+ title="History",
52
+ columns=HISTORY_LIST_COLUMNS,
53
+ rows=_list_rows(entries),
54
+ caption=pluralize(len(entries), "entry", "entries"),
55
+ )
56
+
57
+
58
+ def render_history_entry(entry: CommandHistory, variables: dict | None):
59
+ rows = [
60
+ ("ID", to_text(_short_id(entry), style="entity.id")),
61
+ ("Alias", to_text(entry.alias, style="entity.name")),
62
+ ("Ran At", to_text(entry.ran_at, style="entity.time")),
63
+ ("Exit Code", _format_exit(entry.exit_code)),
64
+ ("Template", to_text(entry.template, style="code.inline")),
65
+ ("Resolved", to_text(entry.resolved, style="code.inline")),
66
+ ]
67
+
68
+ if variables:
69
+ for k, v in variables.items():
70
+ rows.append((f" <{k}>", to_text(v, style="code.inline")))
71
+
72
+ return section(
73
+ title=f"History Entry - {_short_id(entry)}",
74
+ body=kv_table(rows),
75
+ )
76
+
77
+
78
+ def render_history_cleared(count: int, alias: str | None):
79
+ scope = f" for '{alias}'" if alias else ""
80
+ return status_line(
81
+ f"Cleared {pluralize(count, 'entry', 'entries')}{scope}.",
82
+ status="success",
83
+ )
@@ -0,0 +1,52 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ """
5
+ Instructions for installing the snippet into the user's shell configuration file.
6
+ """
7
+
8
+
9
+ bash_path = os.path.join(Path.home(), ".bashrc")
10
+ bash = [
11
+ f'Locate your shell configuration file (usually located at: "{bash_path}").',
12
+ "Add the code snippet to the end of the file.",
13
+ "Restart your shell session for the changes to take effect.",
14
+ ]
15
+
16
+ zsh_path = os.path.join(Path.home(), ".zshrc")
17
+ zsh = [
18
+ f'Locate your shell configuration file (usually located at: "{zsh_path}").',
19
+ "Add the code snippet to the end of the file.",
20
+ "Restart your shell session for the changes to take effect.",
21
+ ]
22
+
23
+ fish_path = os.path.join(Path.home(), ".config", "fish", "config.fish")
24
+ fish = [
25
+ f'Locate your shell configuration file (usually located at: "{fish_path}").',
26
+ "Add the code snippet to the end of the file.",
27
+ "Restart your shell session for the changes to take effect.",
28
+ ]
29
+
30
+ powershell_path = os.path.join(
31
+ Path.home(), "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"
32
+ )
33
+ powershell = [
34
+ f'Locate your powershell configuration file (usually located at: "{powershell_path}"). ',
35
+ "If you do not find the correct configuration file here, you can run the command `$PROFILE` from your powershell terminal to output the exact location.",
36
+ "Add the code snippet to the end of the file.",
37
+ "Restart your shell session for the changes to take effect.",
38
+ ]
39
+
40
+ pwsh = powershell
41
+
42
+ cmd = [
43
+ "cmd.exe integration is only supported as a wrapper script, not a profile snippet.",
44
+ 'Create a file named "cbe.cmd" (the file can be created anywhere on your system).',
45
+ "Paste the snippet into this file and save it.",
46
+ "Ensure the file is on your PATH.",
47
+ "Restart your shell for changes to take effect.",
48
+ ]
49
+
50
+
51
+ def get_instructions(shell: str) -> list[str]:
52
+ return globals()[shell.lower()]
@@ -0,0 +1,57 @@
1
+ from cmdbox.cli.ui.presenters.init_instructions import get_instructions
2
+ from cmdbox.cli.ui.primitives import (
3
+ section,
4
+ to_text,
5
+ stack,
6
+ spacer,
7
+ bullet_list,
8
+ banner,
9
+ )
10
+
11
+
12
+ def render_install_instructions(
13
+ install_snippet: str,
14
+ shell: str,
15
+ *,
16
+ title: str = "Installation Instructions",
17
+ include_help_text: bool = True,
18
+ ):
19
+ snippet = to_text(install_snippet, style="code.block")
20
+ instruction_text = get_instructions(shell)
21
+ instructions = section(
22
+ title="What to do with this snippet",
23
+ body=bullet_list(instruction_text, item_style="ui.muted"),
24
+ border_style="status.info",
25
+ )
26
+ if include_help_text:
27
+ help_text = section(
28
+ title="Automate",
29
+ body=to_text(
30
+ "Re-run the `init` command with the `--install` flag to automatically install the snippet into your configuration file",
31
+ style="ui.muted",
32
+ ),
33
+ )
34
+ else:
35
+ help_text = ""
36
+ body = stack(snippet, spacer(0), instructions, spacer(0), help_text)
37
+ return section(
38
+ title=title,
39
+ body=body,
40
+ border_style="ui.border",
41
+ )
42
+
43
+
44
+ def render_install_success():
45
+ return banner(
46
+ title="Shell integration installed",
47
+ subtitle="Restart your shell for changes to take effect",
48
+ status="success",
49
+ )
50
+
51
+
52
+ def render_shell_output(shell: str):
53
+ return section(
54
+ title="Shell detected",
55
+ body=to_text(shell, style="code.inline"),
56
+ border_style="status.info",
57
+ )
@@ -0,0 +1,144 @@
1
+ from rich.text import Text
2
+
3
+ from cmdbox.cli.ui.primitives import (
4
+ banner,
5
+ section,
6
+ tag_block,
7
+ stack,
8
+ spacer,
9
+ code_block,
10
+ table_panel,
11
+ col,
12
+ )
13
+ from cmdbox.repositories.results import TagAttachResult, TagDetachResult
14
+ from cmdbox.resolve.type_defs import ResolveResult
15
+ from cmdbox.runtime.executor import RunContext
16
+ from cmdbox.runtime.results import ExecutionResult
17
+
18
+
19
+ def render_execution_result(result: ExecutionResult, *, title: str = "Run"):
20
+ ok = result.exit_code == 0
21
+ status = "success" if ok else "error"
22
+
23
+ # Wrap stdout border in error color if status is error
24
+ stdout_border = "ui.border" if ok else "status.error"
25
+ # If ok, wrap stderr border in warning color, otherwise error
26
+ stderr_border = "status.error" if not ok else "status.warning"
27
+
28
+ parts = [
29
+ banner(
30
+ title=title if ok else f"{title} failed",
31
+ subtitle=f"Exit code: {result.exit_code}",
32
+ status=status,
33
+ ),
34
+ spacer(1),
35
+ code_block(
36
+ result.command,
37
+ title="Command",
38
+ border_style="ui.border",
39
+ style="run.command",
40
+ ),
41
+ ]
42
+
43
+ if result.stdout and result.stdout.strip():
44
+ parts += [
45
+ section(
46
+ "STDOUT",
47
+ Text(result.stdout.rstrip("\n"), style="run.stdout"),
48
+ border_style=stdout_border,
49
+ ),
50
+ ]
51
+
52
+ if result.stderr and result.stderr.strip():
53
+ parts += [
54
+ section(
55
+ "STDERR",
56
+ Text(result.stderr.rstrip("\n"), style="run.stderr"),
57
+ border_style=stderr_border,
58
+ ),
59
+ ]
60
+
61
+ return stack(*parts)
62
+
63
+
64
+ def render_preview_result(
65
+ result: ResolveResult,
66
+ *,
67
+ title: str = "Preview",
68
+ show_trace: bool = True,
69
+ ctx: RunContext = None,
70
+ ):
71
+ parts = [
72
+ code_block(result.text, title="Resolved command", border_style="run.command"),
73
+ ]
74
+
75
+ if show_trace and result.trace:
76
+ parts += [spacer(0), _render_trace_steps(result)]
77
+
78
+ if ctx:
79
+ parts += [spacer(0), render_run_context(ctx)]
80
+
81
+ return section(
82
+ title=title,
83
+ body=stack(*parts),
84
+ border_style="ui.border",
85
+ )
86
+
87
+
88
+ def render_run_context(ctx: RunContext, *, title: str = "Run context"):
89
+ return code_block(str(ctx), title=title, border_style="ui.border")
90
+
91
+
92
+ def _render_trace_steps(result: ResolveResult):
93
+ columns = [
94
+ col("Kind", style="run.trace.kind", no_wrap=True),
95
+ col("Key", style="run.trace.key", overflow="fold"),
96
+ col("Expanded to", style="run.trace.value", overflow="fold"),
97
+ ]
98
+
99
+ rows = [(step.kind.name, step.key, step.expanded_to) for step in result.trace]
100
+
101
+ table = table_panel(title="Trace", columns=columns, rows=rows)
102
+ return table
103
+
104
+
105
+ def render_tag_attach_result(result: TagAttachResult):
106
+ added_tag_block = tag_block(tags=result.added)
107
+ existing_tag_block = tag_block(tags=result.existing, style="tag.pill_muted")
108
+ renderable = []
109
+ if len(result.added) > 0:
110
+ added_section = section(
111
+ title=f"{len(result.added)} tags added successfully",
112
+ body=added_tag_block,
113
+ border_style="status.success",
114
+ )
115
+ renderable.append(added_section)
116
+ if len(result.existing) > 0:
117
+ existing_section = section(
118
+ title=f"{len(result.existing)} tags already exist",
119
+ body=existing_tag_block,
120
+ border_style="ui.dim",
121
+ )
122
+ renderable.append(existing_section)
123
+ return stack(*renderable)
124
+
125
+
126
+ def render_tag_detach_result(result: TagDetachResult):
127
+ removed_tag_block = tag_block(tags=result.removed)
128
+ not_attached_tag_block = tag_block(tags=result.not_attached, style="tag.pill_muted")
129
+ renderable = []
130
+ if len(result.removed) > 0:
131
+ removed_section = section(
132
+ title=f"{len(result.removed)} tags removed successfully",
133
+ body=removed_tag_block,
134
+ border_style="status.success",
135
+ )
136
+ renderable.append(removed_section)
137
+ if len(result.not_attached) > 0:
138
+ not_attached_section = section(
139
+ title=f"{len(result.not_attached)} tags not attached",
140
+ body=not_attached_tag_block,
141
+ border_style="ui.dim",
142
+ )
143
+ renderable.append(not_attached_section)
144
+ return stack(*renderable)