vest-build 0.0.1__tar.gz

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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.3
2
+ Name: vest-build
3
+ Version: 0.0.1
4
+ Summary: A Python-based build system for bespoke use
5
+ Author: ascpixi
6
+ Author-email: ascpixi <ascpixi@proton.me>
7
+ Requires-Dist: packaging>=26.0
8
+ Requires-Dist: rich>=15.0.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # 🦺 Vest
13
+ [![image](https://img.shields.io/pypi/v/vest-build.svg)](https://pypi.python.org/pypi/vest-build)
14
+ [![image](https://img.shields.io/pypi/l/vest-build.svg)](https://github.com/ascpixi/vest/blob/main/LICENSE)
15
+ [![image](https://img.shields.io/pypi/pyversions/vest-build.svg)](https://pypi.python.org/pypi/vest-build)
16
+ [![Actions status](https://github.com/ascpixi/vest/workflows/CI/badge.svg)](https://github.com/ascpixi/vest/actions)
17
+
18
+ Vest is a Python-based build system for bespoke use.
19
+
20
+ ## Getting started
21
+ First, install Vest from PyPI:
22
+ ```
23
+ pip install vest-build
24
+ ```
25
+
26
+ Then, create a `build.vest` file! What this file will do depends on your project. Let's say you're building a C project. In this case, your `.vest` file might look like this:
27
+
28
+ ```py
29
+ from vest import *
30
+
31
+ component(
32
+ name = "my-c-project"
33
+ )
34
+
35
+ @task
36
+ def build():
37
+ # Let's turn each C file to an object file...
38
+ for (src, dst) in build_list(
39
+ src = "./src/**/*.c",
40
+ dst = "./obj",
41
+ ext = ".o",
42
+ flatten = True
43
+ ):
44
+ run("gcc", ["-c", src, "-o", dst])
45
+
46
+ # ...then link em' all!
47
+ run("gcc", [
48
+ "-o", "./bin/final",
49
+ *glob("./obj/*.o")
50
+ ])
51
+
52
+ # Tasks should return all of their final artifacts. This can be a string, list of strings, or a dictionary of strings.
53
+ return "./bin/final"
54
+
55
+ @task
56
+ def run():
57
+ # "run" needs a build to execute!
58
+ artifact = self_dependency(build)
59
+
60
+ # Let's run what "build" gave us.
61
+ run(artifact)
62
+ ```
63
+
64
+ In the directory you've created the `vest.build` file in, run `vest -t build`.
65
+
66
+ ## Extensions
67
+ Vest doesn't assume what languages nor frameworks you're using it for. However, there's a couple of ready PyPI packages for known use-cases:
68
+ - [`vest-build-dotnet`](https://github.com/ascpixi/vest-dotnet): support for the .NET SDK.
@@ -0,0 +1,57 @@
1
+ # 🦺 Vest
2
+ [![image](https://img.shields.io/pypi/v/vest-build.svg)](https://pypi.python.org/pypi/vest-build)
3
+ [![image](https://img.shields.io/pypi/l/vest-build.svg)](https://github.com/ascpixi/vest/blob/main/LICENSE)
4
+ [![image](https://img.shields.io/pypi/pyversions/vest-build.svg)](https://pypi.python.org/pypi/vest-build)
5
+ [![Actions status](https://github.com/ascpixi/vest/workflows/CI/badge.svg)](https://github.com/ascpixi/vest/actions)
6
+
7
+ Vest is a Python-based build system for bespoke use.
8
+
9
+ ## Getting started
10
+ First, install Vest from PyPI:
11
+ ```
12
+ pip install vest-build
13
+ ```
14
+
15
+ Then, create a `build.vest` file! What this file will do depends on your project. Let's say you're building a C project. In this case, your `.vest` file might look like this:
16
+
17
+ ```py
18
+ from vest import *
19
+
20
+ component(
21
+ name = "my-c-project"
22
+ )
23
+
24
+ @task
25
+ def build():
26
+ # Let's turn each C file to an object file...
27
+ for (src, dst) in build_list(
28
+ src = "./src/**/*.c",
29
+ dst = "./obj",
30
+ ext = ".o",
31
+ flatten = True
32
+ ):
33
+ run("gcc", ["-c", src, "-o", dst])
34
+
35
+ # ...then link em' all!
36
+ run("gcc", [
37
+ "-o", "./bin/final",
38
+ *glob("./obj/*.o")
39
+ ])
40
+
41
+ # Tasks should return all of their final artifacts. This can be a string, list of strings, or a dictionary of strings.
42
+ return "./bin/final"
43
+
44
+ @task
45
+ def run():
46
+ # "run" needs a build to execute!
47
+ artifact = self_dependency(build)
48
+
49
+ # Let's run what "build" gave us.
50
+ run(artifact)
51
+ ```
52
+
53
+ In the directory you've created the `vest.build` file in, run `vest -t build`.
54
+
55
+ ## Extensions
56
+ Vest doesn't assume what languages nor frameworks you're using it for. However, there's a couple of ready PyPI packages for known use-cases:
57
+ - [`vest-build-dotnet`](https://github.com/ascpixi/vest-dotnet): support for the .NET SDK.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "vest-build"
3
+ version = "0.0.1"
4
+ description = "A Python-based build system for bespoke use"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "ascpixi", email = "ascpixi@proton.me" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "packaging>=26.0",
12
+ "rich>=15.0.0"
13
+ ]
14
+
15
+ [project.scripts]
16
+ vest = "vest.__main__:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.18,<0.10.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [tool.uv.build-backend]
23
+ module-name = "vest"
@@ -0,0 +1,55 @@
1
+ """
2
+ This module defines parts of the component system specification that are recommended to be used in all Vest components.
3
+ This module should only be imported from components, via `from vest import *`.
4
+ """
5
+
6
+ from pkgutil import extend_path
7
+ __path__ = extend_path(__path__, __name__)
8
+
9
+ from vest.spec.core import (
10
+ artifact_dir,
11
+ artifact_dir_path_of,
12
+ clean_artifact_dir,
13
+ artifact_root,
14
+ artifact_root_path_of,
15
+ clean_artifact_root,
16
+ component,
17
+ dependency,
18
+ dependent_on,
19
+ host,
20
+ require_task,
21
+ self_dependency,
22
+ task,
23
+ run,
24
+ run_tool,
25
+ parameter
26
+ )
27
+
28
+ from vest.spec.externals import (
29
+ git_repo,
30
+ GitRepoResolver
31
+ )
32
+
33
+ from vest.spec.types import (
34
+ Component,
35
+ ComponentTask,
36
+ BuildError,
37
+ ExternalRepoResolver
38
+ )
39
+
40
+ from vest.spec.fs import (
41
+ build_list,
42
+ copy,
43
+ copy_many,
44
+ expand_path,
45
+ foreign_file,
46
+ glob,
47
+ make_unique_dir
48
+ )
49
+
50
+ from vest.common.collections import (
51
+ find,
52
+ maybe,
53
+ single,
54
+ flatten
55
+ )
@@ -0,0 +1,221 @@
1
+ import os
2
+ import argparse
3
+ import traceback
4
+ import subprocess
5
+ from rich import print
6
+ from rich.console import Console
7
+ import rich.traceback
8
+
9
+ from vest.common.log import is_agent, log_warn, log_error_hdr, log_success_hdr
10
+ from vest.spec.host import StandaloneHost
11
+ from vest.spec.types import BuildError, UsageError
12
+
13
+ def main():
14
+ console = Console()
15
+ rich.traceback.install()
16
+
17
+ parser = argparse.ArgumentParser()
18
+ parser.add_argument("-t", "--task", help = "The task to run.", required = True)
19
+ parser.add_argument("-p", "--path", default = "./build.vest", help = "The path to the target component.")
20
+ parser.add_argument("-r", "--run", action = "store_true", help = "If set, the 'run_script' of the given task will be invoked.")
21
+ parser.add_argument("--no-cache", action = "store_true", help = "If set, the results of the previous build will be ignored.")
22
+ args, extra_args = parser.parse_known_args()
23
+
24
+ def parse_extra_args():
25
+ transformed: dict[str, str] = {}
26
+
27
+ prev_arg: str | None = None
28
+ expecting_key = True
29
+
30
+ for arg in extra_args:
31
+ is_key = arg.startswith("-")
32
+ if is_key != expecting_key:
33
+ if expecting_key:
34
+ # Two keys right next to each other (e.g --type --arch)
35
+ if prev_arg is not None:
36
+ log_error_hdr(f"Two or more values specified for parameter {prev_arg}.")
37
+ exit(1)
38
+ else:
39
+ log_error_hdr(f"Stray argument {arg} found.")
40
+ exit(1)
41
+ else:
42
+ # Two values right next to each other (e.g. --arch amd64 release)
43
+ log_error_hdr(f"No value found for parameter {prev_arg}.")
44
+ exit(1)
45
+
46
+ if not expecting_key:
47
+ # There isn't really a possibility where we don't have a previous arg here. We're looking for a value, thus we went over the first iteration
48
+ # (which was looking for a key).
49
+ assert prev_arg is not None
50
+
51
+ # This is value! "prev_arg" holds the key, while "arg" holds the value.
52
+ transformed[prev_arg] = arg
53
+
54
+ prev_arg = arg
55
+ expecting_key = not expecting_key
56
+
57
+ return transformed
58
+
59
+ host = StandaloneHost(
60
+ parameters = parse_extra_args(),
61
+ load_cache = not args.no_cache
62
+ )
63
+
64
+ try:
65
+ if os.path.isdir(args.path):
66
+ args.path = os.path.join(args.path, "build.vest")
67
+
68
+ component = host.eval_component(args.path)
69
+ except Exception as ex:
70
+ log_error_hdr("Could not evaluate the component")
71
+ print(f"[red] ╰─> {ex}[/]")
72
+ print()
73
+
74
+ console.print_exception()
75
+ exit(1)
76
+
77
+ if not any(t.name == args.task for t in component.tasks):
78
+ log_error_hdr(f"No task named '{args.task}'.")
79
+ print("[red] ╰─> The defined tasks are:[/]")
80
+
81
+ for task in component.tasks:
82
+ if task.description is not None:
83
+ print(f"[red] - {task.name}: {task.description}[/]")
84
+ else:
85
+ print(f"[red] - {task.name}[/]")
86
+
87
+ exit(1)
88
+
89
+ try:
90
+ task = next(x for x in component.tasks if x.name == args.task)
91
+ artifacts = host.run_task(task)
92
+ except Exception as ex:
93
+ if type(ex) is UsageError:
94
+ # A UsageError indicates that neither Vest or the components in question are at fault - the user might've e.g. not specified a
95
+ # required parameter. In this case, we don't print the stacktrace at all.
96
+ print(" │")
97
+ log_error_hdr(f"Could not build [bold]{component.name}:{args.task}[/]")
98
+ print(f"[red] ╰─> {ex}[/]")
99
+ print()
100
+ exit(1)
101
+
102
+ # https://github.com/Textualize/rich/blob/master/rich/traceback.py#L234
103
+ rich.traceback.Traceback.LEXERS[".vest"] = "python"
104
+
105
+ # Skip the first two frames; which are this script and the component host
106
+ # respectively. We only want to show the faulting components.
107
+ if ex.__traceback__ is not None:
108
+ first = ex.__traceback__
109
+ for i in range(2):
110
+ if ex.__traceback__.tb_next is None:
111
+ ex.__traceback__ = first # revert back to the previous state
112
+ break
113
+
114
+ ex.__traceback__ = ex.__traceback__.tb_next
115
+
116
+ # A BuildError indicates that the cause of the error is incorrect usage of the
117
+ # component system (as opposed to e.g. a bug). In this case, we don't need to
118
+ # show the frames that belong to the component system.
119
+ if type(ex) is BuildError:
120
+ # Filter out stacktraces that are not .vest files (unless it's the last one)
121
+ prev = ex.__traceback__
122
+ current = prev.tb_next
123
+ while current is not None and current.tb_next is not None:
124
+ if not current.tb_frame.f_code.co_filename.endswith(".vest"):
125
+ prev.tb_next = current.tb_next
126
+ current = prev.tb_next
127
+ else:
128
+ prev = current
129
+ current = current.tb_next
130
+
131
+ # Don't show the last frame
132
+ current = ex.__traceback__
133
+ prev = None
134
+
135
+ while current.tb_next is not None:
136
+ prev = current
137
+ current = current.tb_next
138
+
139
+ if prev is not None:
140
+ prev.tb_next = None
141
+
142
+ print(" │")
143
+ log_error_hdr(f"Could not build [bold]{component.name}:{args.task}[/]")
144
+ print(f"[red] ╰─> {ex}[/]")
145
+ print()
146
+
147
+ if type(ex) is subprocess.CalledProcessError:
148
+ stdout = ex.stdout.decode("utf-8") if type(ex.stdout) is bytes else str(ex.stdout)
149
+ stderr = ex.stderr.decode("utf-8") if type(ex.stderr) is bytes else str(ex.stderr)
150
+
151
+ stdout = stdout.strip()
152
+ stderr = stderr.strip()
153
+
154
+ if stdout:
155
+ print(f"[red]Standard output:[/]")
156
+ print(stdout)
157
+ print()
158
+
159
+ if stderr:
160
+ print(f"[red]Standard error:[/]")
161
+ print(stderr)
162
+ print()
163
+
164
+ if not is_agent():
165
+ console.print_exception()
166
+ else:
167
+ traceback.print_exc()
168
+
169
+ print()
170
+ exit(1)
171
+
172
+ print(" │")
173
+ log_success_hdr(f"Component [bold]{component.name}:{args.task}[/] built successfully")
174
+
175
+ if not args.run:
176
+ if type(artifacts) is str:
177
+ print(f" ╰─> You may find the resulting artifact in [bold]{artifacts}[/].")
178
+ elif type(artifacts) is list:
179
+ print(f" ╰─> The following artifacts have been created:")
180
+
181
+ for entry in artifacts:
182
+ print(f" - {entry}")
183
+ elif type(artifacts) is dict:
184
+ print(f" ╰─> The following named artifacts have been created:")
185
+
186
+ for (k, v) in artifacts.items():
187
+ print(f" - [bold]{k}[/]: {v}")
188
+ else:
189
+ print(artifacts) # this should never happen...
190
+
191
+ print()
192
+
193
+ try:
194
+ host.write_cache()
195
+ except:
196
+ print()
197
+ print(" │")
198
+ log_warn(f"Could not write the build cache")
199
+ print(f"[yellow] ╰─> An exception occured while storing the build cache file.[/]")
200
+ print(f"[yellow] This might result in anomalous behavior in future build system invocations.[/]")
201
+ print(f"[yellow] You may delete the 'artifacts' directory if this is the case.[/]")
202
+ print()
203
+ console.print_exception()
204
+
205
+ if args.run:
206
+ if task.run_script == None:
207
+ log_error_hdr(f"Cannot run/debug the artifacts of [bold]{component.name}:{task.name}[/]")
208
+ print(f"[red] ╰─> The component does not define a 'run_script' to invoke.[/]")
209
+ exit(1)
210
+
211
+ run_args = task.run_script(artifacts)
212
+ assert len(run_args) > 0
213
+
214
+ console.show_cursor()
215
+
216
+ # Hand off control to the script, so that we don't mess stuff up with any
217
+ # e.g. signal handlers we may have installed
218
+ os.execvp(run_args[0], run_args)
219
+
220
+ if __name__ == "__main__":
221
+ main()
@@ -0,0 +1,83 @@
1
+ from typing import Callable, Iterable, TypeVar, overload
2
+
3
+ T = TypeVar("T")
4
+ TKey = TypeVar("TKey")
5
+ TValue = TypeVar("TValue")
6
+
7
+ def find(collection: list[T], predicate: Callable[[T], bool]) -> T | None:
8
+ "Finds the first item in the given collection that meets the specified predicate."
9
+ for x in collection:
10
+ if predicate(x):
11
+ return x
12
+
13
+ return None
14
+
15
+ def single(collection: list[T], predicate: Callable[[T], bool]) -> T:
16
+ """
17
+ Picks the first item in the given collection that meets the specified predicate.
18
+ If no item meets the predicate, a `LookupError` is raised.
19
+ """
20
+ for x in collection:
21
+ if predicate(x):
22
+ return x
23
+
24
+ raise LookupError("No value in the given collection matches the predicate.")
25
+
26
+ @overload
27
+ def maybe(value: dict[TKey, TValue], when: bool) -> dict[TKey, TValue]:
28
+ """
29
+ Returns the given `value` if `condition` is `True` - otherwise, returns an empty dictionary.
30
+
31
+ This function is useful when defining conditional collection elements:
32
+ ```
33
+ example = {
34
+ "key1": "always present",
35
+ *maybe({ "key2": "sometimes present" }, when = some_condition)
36
+ }
37
+ ```
38
+ """
39
+ ...
40
+
41
+ @overload
42
+ def maybe(value: T | list[T], when: bool) -> list[T]:
43
+ """
44
+ Returns a 1-element list of the given `value` if `condition` is `True` - otherwise,
45
+ returns an empty list. If `value` is a `list`, it is returned instead.
46
+
47
+ This function is useful when defining conditional collection elements:
48
+ ```
49
+ example = [
50
+ "always present",
51
+ *maybe("sometimes present", when = some_condition)
52
+ ]
53
+ ```
54
+
55
+ ...as opposed to:
56
+ ```
57
+ example = [
58
+ "always present",
59
+ *(["sometimes present"] if some_condition else [])
60
+ ]
61
+ ```
62
+ """
63
+ ...
64
+
65
+ def maybe(value: T | list[T] | dict[TKey, TValue], when: bool) -> list[T] | dict[TKey, TValue]:
66
+ if isinstance(value, dict):
67
+ return value if when else {}
68
+
69
+ if not isinstance(value, list):
70
+ return [value] if when else []
71
+ else:
72
+ return value if when else []
73
+
74
+ def flatten(value: Iterable[list[T]]) -> list[T]:
75
+ """
76
+ Flattens a collection of other, nested lists. For example, the sequence `[[1, 2], [3, 4], [5, 6]]`
77
+ becomes `[1, 2, 3, 4, 5, 6]`.
78
+ """
79
+ result: list[T] = []
80
+ for subarr in value:
81
+ result.extend(subarr)
82
+
83
+ return result
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ def walk_down(start_path: str, end_path: str):
4
+ """
5
+ Returns the paths to traverse when walking down a directory tree, starting at
6
+ `start_path`, and ending with `end_path`.
7
+
8
+ @start_path: The path to being traversal in. Must be a child of `end_path`.
9
+ @end_path: The path to end traversing directories on. Must be a parent of `start_path`.
10
+ """
11
+
12
+ start_path = os.path.abspath(start_path)
13
+ end_path = os.path.abspath(end_path)
14
+
15
+ if not start_path.startswith(end_path):
16
+ raise ValueError(f"End path '{end_path}' is not a parent of start path '{start_path}'")
17
+
18
+ current_path = start_path
19
+ paths = [current_path]
20
+
21
+ while current_path != end_path:
22
+ if current_path == os.path.dirname(current_path): # We've reached the root
23
+ raise ValueError(f"Reached root directory before finding end path '{end_path}'")
24
+
25
+ current_path = os.path.dirname(current_path)
26
+ paths.append(current_path)
27
+
28
+ return paths
@@ -0,0 +1,145 @@
1
+ import os
2
+ import atexit
3
+ import shutil
4
+ import rich
5
+ from rich import print
6
+ from rich.console import Console
7
+ from rich.text import Text
8
+ from rich.table import Table
9
+
10
+ _current_log_indent = 0
11
+ _task_stack: list[str] = []
12
+
13
+ def is_github_actions():
14
+ return "GITHUB_RUN_ID" in os.environ
15
+
16
+ def is_agent():
17
+ return "CLAUDECODE" in os.environ or "GEMINI_CLI" in os.environ or "CURSOR_AGENT" in os.environ
18
+
19
+ def is_non_interactive():
20
+ return is_github_actions() or is_agent()
21
+
22
+ def log_dedent(delta: int = 2):
23
+ global _current_log_indent
24
+ _current_log_indent -= delta
25
+ _task_stack.pop()
26
+
27
+ def log_reset_terminal():
28
+ rich.reconfigure()
29
+ rich.console.Console().show_cursor()
30
+
31
+ def log_deinit():
32
+ """Performs on-exit de-initialization of the logging sub-system."""
33
+ if not is_non_interactive():
34
+ log_reset_terminal()
35
+
36
+ def log_get_effective_width():
37
+ return shutil.get_terminal_size().columns - _current_log_indent
38
+
39
+ def log_dependency(full_name: str, fulfilled = False):
40
+ global _current_log_indent
41
+
42
+ if not fulfilled:
43
+ _current_log_indent += 2
44
+ _task_stack.append(full_name)
45
+
46
+ if is_agent():
47
+ print(f"{full_name} was built before - skipping because of cache." if fulfilled else f"Building {full_name}...")
48
+ return
49
+
50
+ formatted = (
51
+ f"◆ Dependency on [bold]{full_name}[/] [bright_black](fullfilled previously)[/]" if fulfilled else
52
+ f"◇ Building [bold]{full_name}[/]..."
53
+ )
54
+
55
+ if _current_log_indent == 0:
56
+ print(f"[white] {formatted}[/]")
57
+ else:
58
+ print(f"[white] {'├'.ljust(_current_log_indent, '─')} {formatted}[/]")
59
+
60
+ def log(prefix: str, msg: str | Text):
61
+ """
62
+ Logs a message to the console. All arguments can be formatted using Rich markup.
63
+ If executing in a CI environment, no special formatting will be applied.
64
+ """
65
+
66
+ if not isinstance(msg, Text):
67
+ msg = Text.from_markup(msg)
68
+
69
+ if is_non_interactive():
70
+ print(prefix, msg)
71
+ return
72
+
73
+ def create_table(border: Text):
74
+ grid = Table.grid()
75
+ grid.add_column()
76
+ grid.add_column()
77
+ grid.add_column()
78
+ grid.add_column()
79
+
80
+ grid.add_row(
81
+ border,
82
+ Text.from_markup(prefix),
83
+ " ",
84
+ msg
85
+ )
86
+
87
+ return grid
88
+
89
+ console = Console()
90
+ with console.capture() as capture:
91
+ console.print(create_table(Text(" │".ljust(_current_log_indent))))
92
+
93
+ simulated = capture.get()
94
+ print(create_table(
95
+ Text((
96
+ # Make sure the first row, which holds the border, is as large as the table.
97
+ (" │".ljust(_current_log_indent + 2) + "\n") * simulated.count("\n")
98
+ ).rstrip("\n"))
99
+ ))
100
+
101
+ def _get_agent_label(label: str):
102
+ if len(_task_stack) == 0:
103
+ return f"({label}) "
104
+
105
+ return f"({label}@{_task_stack[-1]}) "
106
+
107
+ def log_info(msg: str | Text):
108
+ """Logs an informational message to the console."""
109
+ log("[white bold]info[/]" if not is_non_interactive() else "(info) " if is_github_actions() else _get_agent_label("info"), msg)
110
+
111
+ def log_warn(msg: str | Text):
112
+ """Logs a warning message to the console."""
113
+ if isinstance(msg, Text):
114
+ msg.stylize("#ffffaf")
115
+ else:
116
+ msg = f"[#ffffaf]{msg}[/]"
117
+
118
+ log("[yellow bold]warn[/]" if not is_non_interactive() else "::warning::" if is_github_actions() else _get_agent_label("warning"), msg)
119
+
120
+ def log_error(msg: str | Text):
121
+ """Logs an error message to the console."""
122
+ if isinstance(msg, Text):
123
+ msg.stylize("#ffcbd7")
124
+ else:
125
+ msg = f"[#ffcbd7]{msg}[/]"
126
+
127
+ log("[red bold]error[/]" if not is_non_interactive() else "::error::" if is_github_actions() else _get_agent_label("error"), msg)
128
+
129
+ def log_error_hdr(msg: str):
130
+ "Prints an error header to the console."
131
+ if is_agent():
132
+ print(f"Failure: {msg}")
133
+ return
134
+
135
+ print(f"[black on red] × ERROR [/] [red]{msg}[/]")
136
+
137
+ def log_success_hdr(msg: str):
138
+ "Prints a success header to the console."
139
+ if is_agent():
140
+ print(f"Success: {msg}")
141
+ return
142
+
143
+ print(f"[black on green] ❯ SUCCESS [/] [green]{msg}[/]")
144
+
145
+ atexit.register(log_deinit)