ecdat 0.2.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.
- ecdat/__init__.py +10 -0
- ecdat/__main__.py +5 -0
- ecdat/cli/__init__.py +203 -0
- ecdat/cli/commands/__init__.py +0 -0
- ecdat/cli/commands/about.py +130 -0
- ecdat/cli/commands/demo.py +116 -0
- ecdat/cli/commands/doctor.py +296 -0
- ecdat/cli/commands/help_cmd.py +205 -0
- ecdat/cli/commands/scan.py +228 -0
- ecdat/cli/commands/version_cmd.py +48 -0
- ecdat/cli/parser.py +87 -0
- ecdat/demo_project/auth/login.py +75 -0
- ecdat/demo_project/certs/cert_verify.go +81 -0
- ecdat/demo_project/keyexchange/channel.go +48 -0
- ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
- ecdat/demo_project/payments/payment.py +64 -0
- ecdat/demo_project/quantum/pqc_utils.py +67 -0
- ecdat/demo_project/quantum/slh_signer.py +40 -0
- ecdat/demo_project/tokens/signing.js +54 -0
- ecdat/py.typed +0 -0
- ecdat/services/__init__.py +1 -0
- ecdat/services/crashlog.py +109 -0
- ecdat/services/demo.py +85 -0
- ecdat/services/paths.py +52 -0
- ecdat/services/scanner.py +248 -0
- ecdat/services/viewmodel.py +326 -0
- ecdat/ui/__init__.py +1 -0
- ecdat/ui/art3d.py +136 -0
- ecdat/ui/art_static.py +65 -0
- ecdat/ui/art_text.py +81 -0
- ecdat/ui/banner.py +148 -0
- ecdat/ui/console.py +119 -0
- ecdat/ui/motion.py +64 -0
- ecdat/ui/render.py +486 -0
- ecdat/ui/theme.py +173 -0
- ecdat-0.2.0.dist-info/METADATA +142 -0
- ecdat-0.2.0.dist-info/RECORD +51 -0
- ecdat-0.2.0.dist-info/WHEEL +5 -0
- ecdat-0.2.0.dist-info/entry_points.txt +2 -0
- ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
- ecdat-0.2.0.dist-info/top_level.txt +2 -0
- ecdat_core/__init__.py +6 -0
- ecdat_core/cbom_export.py +287 -0
- ecdat_core/cli.py +202 -0
- ecdat_core/detector.py +273 -0
- ecdat_core/ingestion.py +581 -0
- ecdat_core/models.py +145 -0
- ecdat_core/recommender.py +74 -0
- ecdat_core/risk_engine.py +264 -0
- ecdat_core/signature_loader.py +204 -0
- ecdat_core/signatures.json +692 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""``ecdat doctor`` — environment self-check.
|
|
2
|
+
|
|
3
|
+
Runs a small set of independent checks and prints one row per check with a
|
|
4
|
+
status glyph, a short value, and a one-line fix when something needs
|
|
5
|
+
attention. The output is plain and copy-pasteable into a GitHub issue.
|
|
6
|
+
Exits ``1`` only when at least one check genuinely fails (``\u2717``);
|
|
7
|
+
warnings (``!``) never fail the command.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
NAME = "doctor"
|
|
23
|
+
|
|
24
|
+
_OK = "\u2713" # ✓
|
|
25
|
+
_WARN = "!" # !
|
|
26
|
+
_FAIL = "\u2717" # ✗
|
|
27
|
+
|
|
28
|
+
# Minimum interpreter the app layer supports (see AGENTS.md: 3.10-compatible code).
|
|
29
|
+
_MIN_PYTHON = (3, 10)
|
|
30
|
+
|
|
31
|
+
_ENV_NAMES = ("ECDAT_HOME", "ECDAT_ANIM", "NO_COLOR")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register(subparsers: "argparse._SubParsersAction") -> None:
|
|
35
|
+
"""Attach the ``doctor`` subcommand to *subparsers*."""
|
|
36
|
+
subparsers.add_parser(
|
|
37
|
+
"doctor",
|
|
38
|
+
help="Check your environment for common problems",
|
|
39
|
+
description=(
|
|
40
|
+
"Check Python, platform, install location, git, terminal, "
|
|
41
|
+
"signatures, ECDAT_HOME, and environment overrides. Every row "
|
|
42
|
+
"carries a one-line fix. Exits 1 only when a check fails."
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Check:
|
|
49
|
+
"""A single doctor check result.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
status: ``"ok"``, ``"warn"``, or ``"fail"``.
|
|
53
|
+
label: Short check name (e.g. ``"Python"``).
|
|
54
|
+
detail: Human-readable value for the check.
|
|
55
|
+
fix: One-line remediation hint (empty when no fix is needed).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
status: str
|
|
59
|
+
label: str
|
|
60
|
+
detail: str
|
|
61
|
+
fix: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run(args) -> int:
|
|
65
|
+
"""Run all checks, print the report, and return the exit code."""
|
|
66
|
+
from rich.style import Style
|
|
67
|
+
from rich.table import Table
|
|
68
|
+
from rich.text import Text
|
|
69
|
+
|
|
70
|
+
from ecdat.ui.console import make_console
|
|
71
|
+
from ecdat.ui.theme import PALETTE
|
|
72
|
+
|
|
73
|
+
no_color = True if getattr(args, "no_color", False) else None
|
|
74
|
+
console = make_console(no_color=no_color)
|
|
75
|
+
|
|
76
|
+
checks = _run_checks(console)
|
|
77
|
+
|
|
78
|
+
table = Table(box=None, show_header=False, padding=(0, 2), expand=False)
|
|
79
|
+
table.add_column(no_wrap=True)
|
|
80
|
+
table.add_column(no_wrap=True)
|
|
81
|
+
table.add_column(overflow="fold")
|
|
82
|
+
|
|
83
|
+
for check in checks:
|
|
84
|
+
table.add_row(
|
|
85
|
+
_status_text(check.status),
|
|
86
|
+
Text(check.label),
|
|
87
|
+
_detail_text(check),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
console.print(
|
|
91
|
+
Text(
|
|
92
|
+
"ECDAT doctor \u2014 environment report",
|
|
93
|
+
style=Style(color=PALETTE.accent, bold=True),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
console.print(table)
|
|
97
|
+
|
|
98
|
+
failed = [c for c in checks if c.status == "fail"]
|
|
99
|
+
warned = [c for c in checks if c.status == "warn"]
|
|
100
|
+
if failed:
|
|
101
|
+
console.print(
|
|
102
|
+
Text(
|
|
103
|
+
f"{len(failed)} check(s) failed \u2014 see fixes above.",
|
|
104
|
+
style=Style(color=PALETTE.critical, bold=True),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return 1
|
|
108
|
+
if warned:
|
|
109
|
+
console.print(
|
|
110
|
+
Text(
|
|
111
|
+
"No blocking problems found.",
|
|
112
|
+
style=Style(color=PALETTE.safe),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
return 0
|
|
116
|
+
console.print(Text("All checks passed.", style=Style(color=PALETTE.safe)))
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Individual checks
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_checks(console) -> list[Check]:
|
|
126
|
+
"""Collect every environment check, ordered for readability."""
|
|
127
|
+
return [
|
|
128
|
+
_check_python(),
|
|
129
|
+
_check_platform(),
|
|
130
|
+
_check_install_location(),
|
|
131
|
+
_check_git(),
|
|
132
|
+
_check_terminal(console),
|
|
133
|
+
_check_signatures(),
|
|
134
|
+
_check_home_writable(),
|
|
135
|
+
_check_env_overrides(),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_python() -> Check:
|
|
140
|
+
version = platform.python_version()
|
|
141
|
+
impl = platform.python_implementation()
|
|
142
|
+
if sys.version_info >= _MIN_PYTHON:
|
|
143
|
+
return Check("ok", "Python", f"{version} ({impl})")
|
|
144
|
+
requirement = ".".join(str(part) for part in _MIN_PYTHON)
|
|
145
|
+
return Check(
|
|
146
|
+
"fail",
|
|
147
|
+
"Python",
|
|
148
|
+
f"{version} ({impl}) \u2014 too old",
|
|
149
|
+
f"Install Python {requirement} or newer.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_platform() -> Check:
|
|
154
|
+
system = platform.system() or "unknown"
|
|
155
|
+
release = platform.release() or "?"
|
|
156
|
+
machine = platform.machine() or "?"
|
|
157
|
+
return Check("ok", "Platform", f"{system} {release} ({machine})")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _check_install_location() -> Check:
|
|
161
|
+
import ecdat
|
|
162
|
+
|
|
163
|
+
location = Path(ecdat.__file__).resolve().parent
|
|
164
|
+
return Check("ok", "Install", str(location))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_git() -> Check:
|
|
168
|
+
git_path = shutil.which("git")
|
|
169
|
+
if git_path is None:
|
|
170
|
+
return Check(
|
|
171
|
+
"warn",
|
|
172
|
+
"git",
|
|
173
|
+
"not found on PATH",
|
|
174
|
+
"Install git to enable repository URL scans.",
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
proc = subprocess.run(
|
|
178
|
+
[git_path, "--version"],
|
|
179
|
+
capture_output=True,
|
|
180
|
+
text=True,
|
|
181
|
+
timeout=5,
|
|
182
|
+
)
|
|
183
|
+
output = (proc.stdout or proc.stderr).strip()
|
|
184
|
+
version = output.splitlines()[0] if output else "present"
|
|
185
|
+
if proc.returncode != 0:
|
|
186
|
+
return Check(
|
|
187
|
+
"warn",
|
|
188
|
+
"git",
|
|
189
|
+
f"{git_path} (version check exited {proc.returncode})",
|
|
190
|
+
"Verify git runs: git --version",
|
|
191
|
+
)
|
|
192
|
+
return Check("ok", "git", f"{version} ({git_path})")
|
|
193
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
194
|
+
return Check(
|
|
195
|
+
"warn",
|
|
196
|
+
"git",
|
|
197
|
+
f"{git_path} (version check failed: {exc})",
|
|
198
|
+
"Verify git runs: git --version",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_terminal(console) -> Check:
|
|
203
|
+
try:
|
|
204
|
+
size = console.size
|
|
205
|
+
dimensions = f"{size.width}x{size.height}"
|
|
206
|
+
except Exception: # noqa: BLE001 - size is best-effort diagnostics
|
|
207
|
+
dimensions = "?"
|
|
208
|
+
encoding = sys.stdout.encoding or "?"
|
|
209
|
+
color_system = console.color_system or "none"
|
|
210
|
+
term = os.environ.get("TERM") or "(unset)"
|
|
211
|
+
detail = (
|
|
212
|
+
f"interactive={sys.stdout.isatty()}, size={dimensions}, "
|
|
213
|
+
f"color={color_system}, encoding={encoding}, TERM={term}"
|
|
214
|
+
)
|
|
215
|
+
return Check("ok", "Terminal", detail)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _check_signatures() -> Check:
|
|
219
|
+
try:
|
|
220
|
+
from ecdat_core.signature_loader import get_all_signatures
|
|
221
|
+
|
|
222
|
+
count = len(get_all_signatures())
|
|
223
|
+
except Exception as exc: # noqa: BLE001 - report any load failure verbatim
|
|
224
|
+
return Check(
|
|
225
|
+
"fail",
|
|
226
|
+
"signatures",
|
|
227
|
+
f"failed to load: {exc}",
|
|
228
|
+
"Reinstall ECDAT: pip install --force-reinstall ecdat",
|
|
229
|
+
)
|
|
230
|
+
return Check("ok", "signatures", f"{count} entries loaded")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _check_home_writable() -> Check:
|
|
234
|
+
from ecdat.services.paths import home_dir
|
|
235
|
+
|
|
236
|
+
home = home_dir()
|
|
237
|
+
try:
|
|
238
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
fd, tmp_name = tempfile.mkstemp(dir=str(home), prefix=".ecdat-write-test-")
|
|
240
|
+
os.close(fd)
|
|
241
|
+
try:
|
|
242
|
+
os.unlink(tmp_name)
|
|
243
|
+
except OSError:
|
|
244
|
+
pass
|
|
245
|
+
except OSError as exc:
|
|
246
|
+
return Check(
|
|
247
|
+
"fail",
|
|
248
|
+
"ECDAT_HOME",
|
|
249
|
+
f"{home} is not writable: {exc}",
|
|
250
|
+
"Set ECDAT_HOME to a writable directory.",
|
|
251
|
+
)
|
|
252
|
+
return Check("ok", "ECDAT_HOME", f"{home} (writable)")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _check_env_overrides() -> Check:
|
|
256
|
+
parts = []
|
|
257
|
+
for name in _ENV_NAMES:
|
|
258
|
+
value = os.environ.get(name)
|
|
259
|
+
parts.append(f"{name}={value}" if value else f"{name} (unset)")
|
|
260
|
+
return Check("ok", "env", ", ".join(parts))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# Rendering helpers
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _status_text(status: str):
|
|
269
|
+
"""Return the coloured status glyph for *status*."""
|
|
270
|
+
from rich.style import Style
|
|
271
|
+
from rich.text import Text
|
|
272
|
+
|
|
273
|
+
from ecdat.ui.theme import PALETTE
|
|
274
|
+
|
|
275
|
+
if status == "fail":
|
|
276
|
+
return Text(_FAIL, style=Style(color=PALETTE.critical, bold=True))
|
|
277
|
+
if status == "warn":
|
|
278
|
+
return Text(_WARN, style=Style(color=PALETTE.medium, bold=True))
|
|
279
|
+
return Text(_OK, style=Style(color=PALETTE.safe, bold=True))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _detail_text(check: Check):
|
|
283
|
+
"""Return the detail string plus an optional dimmed fix hint."""
|
|
284
|
+
from rich.style import Style
|
|
285
|
+
from rich.text import Text
|
|
286
|
+
|
|
287
|
+
from ecdat.ui.theme import PALETTE
|
|
288
|
+
|
|
289
|
+
text = Text(check.detail)
|
|
290
|
+
if check.fix:
|
|
291
|
+
text.append(" \u2192 ")
|
|
292
|
+
text.append(
|
|
293
|
+
check.fix,
|
|
294
|
+
style=Style(color=PALETTE.muted, italic=True),
|
|
295
|
+
)
|
|
296
|
+
return text
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""``ecdat help`` — a friendly overview of every command.
|
|
2
|
+
|
|
3
|
+
The command table is built by introspecting the argument parser, so a newly
|
|
4
|
+
registered subcommand shows up here automatically. ``ecdat help <command>``
|
|
5
|
+
renders that subcommand's own ``argparse`` help inside a panel (as plain
|
|
6
|
+
:class:`rich.text.Text`, never markup).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
NAME = "help"
|
|
14
|
+
|
|
15
|
+
_QUICK_START = (
|
|
16
|
+
"ecdat demo",
|
|
17
|
+
"ecdat scan . --fail-on high",
|
|
18
|
+
"ecdat scan https://github.com/acme/app --git-url",
|
|
19
|
+
"ecdat help scan",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Optional per-command examples; unknown/new commands fall back to "ecdat <name>".
|
|
23
|
+
_EXAMPLES: dict[str, str] = {
|
|
24
|
+
"scan": "ecdat scan . --fail-on high",
|
|
25
|
+
"demo": "ecdat demo",
|
|
26
|
+
"help": "ecdat help scan",
|
|
27
|
+
"doctor": "ecdat doctor",
|
|
28
|
+
"about": "ecdat about",
|
|
29
|
+
"version": "ecdat version",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_EXIT_CODES = (
|
|
33
|
+
("0", "success"),
|
|
34
|
+
("1", "findings at or above --fail-on"),
|
|
35
|
+
("2", "usage or validation error"),
|
|
36
|
+
("3", "scan, runtime, or unexpected error"),
|
|
37
|
+
("130", "interrupted (Ctrl-C)"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register(subparsers: "argparse._SubParsersAction") -> None:
|
|
42
|
+
"""Attach the ``help`` subcommand to *subparsers*."""
|
|
43
|
+
parser = subparsers.add_parser(
|
|
44
|
+
"help",
|
|
45
|
+
help="List all commands, or show detailed help for one",
|
|
46
|
+
description=(
|
|
47
|
+
"List every ECDAT command with an example, or show the full "
|
|
48
|
+
"argparse help for a single command: ecdat help <command>."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"topic",
|
|
53
|
+
nargs="?",
|
|
54
|
+
default=None,
|
|
55
|
+
metavar="COMMAND",
|
|
56
|
+
help="Command to show detailed help for",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run(args) -> int:
|
|
61
|
+
"""Print the command overview or one command's help; return the exit code."""
|
|
62
|
+
from rich.panel import Panel
|
|
63
|
+
from rich.style import Style
|
|
64
|
+
from rich.text import Text
|
|
65
|
+
|
|
66
|
+
from ecdat import __version__
|
|
67
|
+
from ecdat.cli.parser import build_parser
|
|
68
|
+
from ecdat.ui.banner import render_banner
|
|
69
|
+
from ecdat.ui.console import make_console
|
|
70
|
+
from ecdat.ui.theme import PALETTE
|
|
71
|
+
|
|
72
|
+
no_color = True if getattr(args, "no_color", False) else None
|
|
73
|
+
out_console = make_console(no_color=no_color)
|
|
74
|
+
err_console = make_console(stderr=True, no_color=no_color)
|
|
75
|
+
|
|
76
|
+
parser = build_parser()
|
|
77
|
+
commands, choices = _registry(parser)
|
|
78
|
+
topic = getattr(args, "topic", None)
|
|
79
|
+
|
|
80
|
+
if topic is None:
|
|
81
|
+
out_console.print(render_banner(version=__version__))
|
|
82
|
+
out_console.print(Text())
|
|
83
|
+
out_console.print(_commands_table(commands))
|
|
84
|
+
out_console.print(Text())
|
|
85
|
+
out_console.print(
|
|
86
|
+
Text("Quick start", style=Style(color=PALETTE.accent, bold=True))
|
|
87
|
+
)
|
|
88
|
+
for line in _QUICK_START:
|
|
89
|
+
out_console.print(Text(f" {line}"))
|
|
90
|
+
out_console.print(Text())
|
|
91
|
+
out_console.print(_exit_codes_table())
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
if topic not in choices:
|
|
95
|
+
valid = ", ".join(name for name, _ in commands)
|
|
96
|
+
err_console.print(
|
|
97
|
+
Text(
|
|
98
|
+
f"Error: unknown command {topic!r}. Valid commands: {valid}",
|
|
99
|
+
style=Style(color=PALETTE.critical, bold=True),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return 2
|
|
103
|
+
|
|
104
|
+
out_console.print(
|
|
105
|
+
Panel(
|
|
106
|
+
Text(choices[topic].format_help()),
|
|
107
|
+
title=f"ecdat {topic}",
|
|
108
|
+
title_align="left",
|
|
109
|
+
border_style=PALETTE.border,
|
|
110
|
+
padding=(1, 2),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Parser introspection
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _registry(
|
|
122
|
+
parser: argparse.ArgumentParser,
|
|
123
|
+
) -> tuple[list[tuple[str, str]], dict[str, argparse.ArgumentParser]]:
|
|
124
|
+
"""Return ``[(name, summary), ...]`` and ``name -> subparser`` for *parser*.
|
|
125
|
+
|
|
126
|
+
Reads argparse's subparser action so the help table never drifts from the
|
|
127
|
+
real command set.
|
|
128
|
+
"""
|
|
129
|
+
action = next(
|
|
130
|
+
(a for a in parser._actions if isinstance(a, argparse._SubParsersAction)),
|
|
131
|
+
None,
|
|
132
|
+
)
|
|
133
|
+
if action is None: # pragma: no cover - build_parser always adds subparsers
|
|
134
|
+
return [], {}
|
|
135
|
+
|
|
136
|
+
rows: list[tuple[str, str]] = []
|
|
137
|
+
for choice in getattr(action, "_choices_actions", []):
|
|
138
|
+
rows.append((choice.dest, choice.help or ""))
|
|
139
|
+
return rows, dict(action.choices)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Renderers (Rich imported lazily by the callers above)
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _commands_table(commands: list[tuple[str, str]]):
|
|
148
|
+
"""Build the rounded command/example table."""
|
|
149
|
+
from rich import box
|
|
150
|
+
from rich.style import Style
|
|
151
|
+
from rich.table import Table
|
|
152
|
+
from rich.text import Text
|
|
153
|
+
|
|
154
|
+
from ecdat.ui.theme import PALETTE
|
|
155
|
+
|
|
156
|
+
table = Table(
|
|
157
|
+
title="Commands",
|
|
158
|
+
title_style=f"bold {PALETTE.accent}",
|
|
159
|
+
title_justify="left",
|
|
160
|
+
border_style=PALETTE.border,
|
|
161
|
+
header_style=Style(color=PALETTE.muted, bold=True),
|
|
162
|
+
box=box.ROUNDED,
|
|
163
|
+
expand=False,
|
|
164
|
+
)
|
|
165
|
+
table.add_column("Command", no_wrap=True)
|
|
166
|
+
table.add_column("Summary")
|
|
167
|
+
table.add_column("Example", no_wrap=True)
|
|
168
|
+
|
|
169
|
+
for name, summary in commands:
|
|
170
|
+
example = _EXAMPLES.get(name, f"ecdat {name}")
|
|
171
|
+
table.add_row(
|
|
172
|
+
Text(f"ecdat {name}", style=Style(color=PALETTE.accent, bold=True)),
|
|
173
|
+
Text(summary),
|
|
174
|
+
Text(example),
|
|
175
|
+
)
|
|
176
|
+
return table
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _exit_codes_table():
|
|
180
|
+
"""Build the rounded exit-code reference table."""
|
|
181
|
+
from rich import box
|
|
182
|
+
from rich.style import Style
|
|
183
|
+
from rich.table import Table
|
|
184
|
+
from rich.text import Text
|
|
185
|
+
|
|
186
|
+
from ecdat.ui.theme import PALETTE
|
|
187
|
+
|
|
188
|
+
table = Table(
|
|
189
|
+
title="Exit codes",
|
|
190
|
+
title_style=f"bold {PALETTE.accent}",
|
|
191
|
+
title_justify="left",
|
|
192
|
+
border_style=PALETTE.border,
|
|
193
|
+
header_style=Style(color=PALETTE.muted, bold=True),
|
|
194
|
+
box=box.ROUNDED,
|
|
195
|
+
expand=False,
|
|
196
|
+
)
|
|
197
|
+
table.add_column("Code", no_wrap=True)
|
|
198
|
+
table.add_column("Meaning")
|
|
199
|
+
|
|
200
|
+
for code, meaning in _EXIT_CODES:
|
|
201
|
+
table.add_row(
|
|
202
|
+
Text(code, style=Style(color=PALETTE.accent, bold=True)),
|
|
203
|
+
Text(meaning),
|
|
204
|
+
)
|
|
205
|
+
return table
|