smartcli-toolkit 0.1.3__tar.gz → 0.1.4__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.
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/PKG-INFO +14 -1
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/README.md +9 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/pyproject.toml +8 -1
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/__init__.py +1 -1
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/PKG-INFO +14 -1
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/SOURCES.txt +1 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/requires.txt +6 -0
- smartcli_toolkit-0.1.4/tests/test_golden_frames.py +161 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/LICENSE +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/setup.cfg +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/pty_backend.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/py.typed +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/readiness.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/screen_model.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/session.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/snapshot.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/dependency_links.txt +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/top_level.txt +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_degenerate_inputs.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_doc_counts.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_fx_contract.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_readiness.py +0 -0
- {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_vendor_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartcli-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core).
|
|
5
5
|
Author: dwgx
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,10 @@ Provides-Extra: all
|
|
|
38
38
|
Requires-Dist: pyfiglet>=1.0.0; extra == "all"
|
|
39
39
|
Requires-Dist: Pillow>=10.0.0; extra == "all"
|
|
40
40
|
Requires-Dist: wcwidth>=0.2.0; extra == "all"
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
41
45
|
Dynamic: license-file
|
|
42
46
|
|
|
43
47
|
<!-- Language: English | [简体中文](docs/i18n/README.zh-Hans.md) | [繁體中文](docs/i18n/README.zh-Hant.md) | [日本語](docs/i18n/README.ja.md) | [한국어](docs/i18n/README.ko.md) -->
|
|
@@ -51,6 +55,7 @@ Dynamic: license-file
|
|
|
51
55
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
52
56
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
53
57
|
[](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
|
|
58
|
+
[](https://codecov.io/gh/dwgx/SmartCLI)
|
|
54
59
|
[](LICENSE)
|
|
55
60
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
56
61
|
[](#features)
|
|
@@ -199,6 +204,14 @@ python skills/drive-tui/scripts/tui.py snapshot --id <SID>
|
|
|
199
204
|
python skills/drive-tui/scripts/tui.py close --id <SID>
|
|
200
205
|
```
|
|
201
206
|
|
|
207
|
+
Or drive from any MCP client — the same verbs as MCP tools, with the
|
|
208
|
+
per-session token attached automatically:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install "smartcli-toolkit[mcp]"
|
|
212
|
+
python skills/drive-tui/scripts/mcp_server.py # stdio MCP server
|
|
213
|
+
```
|
|
214
|
+
|
|
202
215
|
### As a library
|
|
203
216
|
|
|
204
217
|
The shared core is importable directly:
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
10
10
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
11
11
|
[](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
|
|
12
|
+
[](https://codecov.io/gh/dwgx/SmartCLI)
|
|
12
13
|
[](LICENSE)
|
|
13
14
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
14
15
|
[](#features)
|
|
@@ -157,6 +158,14 @@ python skills/drive-tui/scripts/tui.py snapshot --id <SID>
|
|
|
157
158
|
python skills/drive-tui/scripts/tui.py close --id <SID>
|
|
158
159
|
```
|
|
159
160
|
|
|
161
|
+
Or drive from any MCP client — the same verbs as MCP tools, with the
|
|
162
|
+
per-session token attached automatically:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pip install "smartcli-toolkit[mcp]"
|
|
166
|
+
python skills/drive-tui/scripts/mcp_server.py # stdio MCP server
|
|
167
|
+
```
|
|
168
|
+
|
|
160
169
|
### As a library
|
|
161
170
|
|
|
162
171
|
The shared core is importable directly:
|
|
@@ -30,7 +30,7 @@ build-backend = "setuptools.build_meta"
|
|
|
30
30
|
|
|
31
31
|
[project]
|
|
32
32
|
name = "smartcli-toolkit"
|
|
33
|
-
version = "0.1.
|
|
33
|
+
version = "0.1.4"
|
|
34
34
|
description = "Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core)."
|
|
35
35
|
readme = "README.md"
|
|
36
36
|
license = "MIT"
|
|
@@ -96,6 +96,13 @@ art = ["pyfiglet>=1.0.0"] # real FIGlet fonts (else built-in block font)
|
|
|
96
96
|
image = ["Pillow>=10.0.0"] # real raster images (else PNM/procedural demo)
|
|
97
97
|
width = ["wcwidth>=0.2.0"] # authoritative cell width (else unicodedata)
|
|
98
98
|
all = ["pyfiglet>=1.0.0", "Pillow>=10.0.0", "wcwidth>=0.2.0"]
|
|
99
|
+
# MCP server over the drive-tui daemon verb surface (skills/drive-tui/scripts/
|
|
100
|
+
# mcp_server.py). Lets any MCP client drive interactive TUIs. Optional — the
|
|
101
|
+
# CLI and library work without it.
|
|
102
|
+
mcp = ["mcp>=1.0"]
|
|
103
|
+
# Dev-only: multi-process coverage over the script-style suite (see
|
|
104
|
+
# tools/coverage_run.py + .coveragerc). Not needed to run SmartCLI.
|
|
105
|
+
dev = ["coverage[toml]>=7.0"]
|
|
99
106
|
|
|
100
107
|
[tool.setuptools]
|
|
101
108
|
# Only the shared core is a clean installable package (see HONEST NOTE above).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartcli-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pluggable-PTY + pyte screen model + semantic snapshot / readiness core for driving interactive terminal programs (SmartCLI shared core).
|
|
5
5
|
Author: dwgx
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,10 @@ Provides-Extra: all
|
|
|
38
38
|
Requires-Dist: pyfiglet>=1.0.0; extra == "all"
|
|
39
39
|
Requires-Dist: Pillow>=10.0.0; extra == "all"
|
|
40
40
|
Requires-Dist: wcwidth>=0.2.0; extra == "all"
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
41
45
|
Dynamic: license-file
|
|
42
46
|
|
|
43
47
|
<!-- Language: English | [简体中文](docs/i18n/README.zh-Hans.md) | [繁體中文](docs/i18n/README.zh-Hant.md) | [日本語](docs/i18n/README.ja.md) | [한국어](docs/i18n/README.ko.md) -->
|
|
@@ -51,6 +55,7 @@ Dynamic: license-file
|
|
|
51
55
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
52
56
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
53
57
|
[](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
|
|
58
|
+
[](https://codecov.io/gh/dwgx/SmartCLI)
|
|
54
59
|
[](LICENSE)
|
|
55
60
|
[](https://pypi.org/project/smartcli-toolkit/)
|
|
56
61
|
[](#features)
|
|
@@ -199,6 +204,14 @@ python skills/drive-tui/scripts/tui.py snapshot --id <SID>
|
|
|
199
204
|
python skills/drive-tui/scripts/tui.py close --id <SID>
|
|
200
205
|
```
|
|
201
206
|
|
|
207
|
+
Or drive from any MCP client — the same verbs as MCP tools, with the
|
|
208
|
+
per-session token attached automatically:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install "smartcli-toolkit[mcp]"
|
|
212
|
+
python skills/drive-tui/scripts/mcp_server.py # stdio MCP server
|
|
213
|
+
```
|
|
214
|
+
|
|
202
215
|
### As a library
|
|
203
216
|
|
|
204
217
|
The shared core is importable directly:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""test_golden_frames.py — golden-frame snapshot regression for tui-ui widgets.
|
|
3
|
+
|
|
4
|
+
Every registered widget is rendered to a DETERMINISTIC ANSI frame (its
|
|
5
|
+
``sample(theme)`` composed into a fixed-size ``Page``, exactly the path
|
|
6
|
+
``python -m ui demo`` uses) and compared byte-for-byte against a committed
|
|
7
|
+
baseline in ``tests/golden/<key>.txt``. This locks the 15 widgets against silent
|
|
8
|
+
visual regressions — today only degenerate-input crashes and the fx frame
|
|
9
|
+
contract are guarded; widget *output* was not.
|
|
10
|
+
|
|
11
|
+
Pure/in-memory: it imports the ui package and renders strings, never spawns a
|
|
12
|
+
process. Deterministic: widgets carry no random/time dependency (verified), and
|
|
13
|
+
the test renders each widget TWICE and fails if the two disagree, so a
|
|
14
|
+
non-deterministic widget can never silently bake flakiness into a baseline.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python tests/test_golden_frames.py # check against baselines (exit 1 on drift)
|
|
18
|
+
python tests/test_golden_frames.py --update # (re)write baselines from current output
|
|
19
|
+
python tests/test_golden_frames.py badge card # limit to named widgets
|
|
20
|
+
|
|
21
|
+
The baselines are meant to be reviewed in the diff: when a widget's look changes
|
|
22
|
+
on purpose, run --update and the PR shows exactly which cells moved.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
30
|
+
sys.path.insert(0, str(ROOT / "skills" / "tui-ui"))
|
|
31
|
+
|
|
32
|
+
# Force UTF-8 stdout: baselines and diffs contain box-drawing + CJK, which crash
|
|
33
|
+
# on a legacy Windows codepage (CP936) otherwise.
|
|
34
|
+
try:
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
from ui import registry # noqa: E402
|
|
40
|
+
from ui.box import Box # noqa: E402
|
|
41
|
+
from ui.core import get_theme # noqa: E402
|
|
42
|
+
from ui.layout import Page # noqa: E402
|
|
43
|
+
|
|
44
|
+
GOLDEN_DIR = ROOT / "tests" / "golden"
|
|
45
|
+
|
|
46
|
+
# Fixed render parameters — must stay constant or every baseline shifts. Chosen
|
|
47
|
+
# wide/tall enough that no widget sample clips at its default.
|
|
48
|
+
WIDTH, HEIGHT = 60, 12
|
|
49
|
+
THEME = "dashboard"
|
|
50
|
+
|
|
51
|
+
results: list[tuple[str, bool, str]] = []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def record(name: str, ok: bool, detail: str = "") -> None:
|
|
55
|
+
results.append((name, ok, detail))
|
|
56
|
+
print(f"{'PASS' if ok else 'FAIL'} {name}" + (f" -- {detail}" if detail else ""))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def render_widget(cls) -> str:
|
|
60
|
+
"""Render ONE widget to a deterministic ANSI frame — mirrors ui.cli.cmd_demo:
|
|
61
|
+
sample(theme) -> Box -> Page.to_ansi()."""
|
|
62
|
+
theme = get_theme(THEME)
|
|
63
|
+
widget = cls.sample(theme)
|
|
64
|
+
page = Page(Box(content=widget, padding=(0, 0), bg=theme.bg),
|
|
65
|
+
width=WIDTH, height=HEIGHT, bg=theme.bg)
|
|
66
|
+
return page.to_ansi()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _baseline_path(key: str) -> Path:
|
|
70
|
+
return GOLDEN_DIR / f"{key}.txt"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_lf(path: Path) -> str:
|
|
74
|
+
"""Read a baseline, normalizing any CRLF to LF so the comparison is
|
|
75
|
+
platform-independent (a Windows checkout may present CRLF; the rendered
|
|
76
|
+
frame is always LF)."""
|
|
77
|
+
return path.read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _first_diff(a: str, b: str) -> str:
|
|
81
|
+
"""Human-pointable description of where two frames first differ."""
|
|
82
|
+
al, bl = a.splitlines(), b.splitlines()
|
|
83
|
+
if len(al) != len(bl):
|
|
84
|
+
return f"line count {len(al)} != baseline {len(bl)}"
|
|
85
|
+
for i, (x, y) in enumerate(zip(al, bl)):
|
|
86
|
+
if x != y:
|
|
87
|
+
col = next((j for j, (cx, cy) in enumerate(zip(x, y)) if cx != cy),
|
|
88
|
+
min(len(x), len(y)))
|
|
89
|
+
return f"line {i} differs at col {col}"
|
|
90
|
+
return "identical length, content differs (trailing?)"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_widget(cls, update: bool) -> None:
|
|
94
|
+
key = cls.key
|
|
95
|
+
# Determinism gate: render twice, must be byte-identical. A widget that
|
|
96
|
+
# depends on time/random would trip here instead of baking flakiness in.
|
|
97
|
+
frame1 = render_widget(cls)
|
|
98
|
+
frame2 = render_widget(cls)
|
|
99
|
+
if frame1 != frame2:
|
|
100
|
+
record(f"golden {key}", False,
|
|
101
|
+
f"NON-DETERMINISTIC render ({_first_diff(frame1, frame2)}) — "
|
|
102
|
+
"fix the widget or seed it before baselining")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
path = _baseline_path(key)
|
|
106
|
+
if update:
|
|
107
|
+
GOLDEN_DIR.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
existed = path.exists()
|
|
109
|
+
old = _read_lf(path) if existed else None
|
|
110
|
+
# Write with LF newlines explicitly (newline="") so a Windows run doesn't
|
|
111
|
+
# bake CRLF into the baseline — the rendered frame uses \n, and git
|
|
112
|
+
# normalizes these files to LF (.gitattributes), so the baseline must be
|
|
113
|
+
# LF on every platform or the check would drift CRLF-vs-LF across OSes.
|
|
114
|
+
with open(path, "w", encoding="utf-8", newline="") as fh:
|
|
115
|
+
fh.write(frame1)
|
|
116
|
+
state = "unchanged" if old == frame1 else ("updated" if existed else "created")
|
|
117
|
+
record(f"golden {key}", True, f"baseline {state}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if not path.exists():
|
|
121
|
+
record(f"golden {key}", False,
|
|
122
|
+
f"NO baseline at {path.relative_to(ROOT)} — run with --update")
|
|
123
|
+
return
|
|
124
|
+
baseline = _read_lf(path)
|
|
125
|
+
if frame1 == baseline:
|
|
126
|
+
record(f"golden {key}", True, f"matches baseline ({len(frame1)} bytes)")
|
|
127
|
+
else:
|
|
128
|
+
record(f"golden {key}", False,
|
|
129
|
+
f"DRIFT vs baseline: {_first_diff(frame1, baseline)} "
|
|
130
|
+
"(intentional? run --update and review the diff)")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(argv: list[str]) -> int:
|
|
134
|
+
update = "--update" in argv
|
|
135
|
+
only = {a for a in argv if not a.startswith("-")}
|
|
136
|
+
|
|
137
|
+
registry.load_all()
|
|
138
|
+
for mod, _tb in registry.load_errors():
|
|
139
|
+
print(f"warning: widget module {mod} failed to import", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
widgets = [c for c in registry.all_widgets() if not only or c.key in only]
|
|
142
|
+
if not widgets:
|
|
143
|
+
print(f"error: no widgets matched {only or '(all)'}", file=sys.stderr)
|
|
144
|
+
return 2
|
|
145
|
+
|
|
146
|
+
for cls in widgets:
|
|
147
|
+
check_widget(cls, update)
|
|
148
|
+
|
|
149
|
+
failed = [r for r in results if not r[1]]
|
|
150
|
+
print(f"\n{len(results) - len(failed)}/{len(results)} widgets "
|
|
151
|
+
f"{'baselined' if update else 'match golden frames'} "
|
|
152
|
+
f"(size {WIDTH}x{HEIGHT}, theme {THEME})")
|
|
153
|
+
if failed and not update:
|
|
154
|
+
print("golden-frame drift — review, then `--update` if intended:")
|
|
155
|
+
for name, _ok, detail in failed:
|
|
156
|
+
print(f" - {name}: {detail}")
|
|
157
|
+
return 1 if (failed and not update) else 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
sys.exit(main(sys.argv[1:]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|