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.
Files changed (23) hide show
  1. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/PKG-INFO +14 -1
  2. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/README.md +9 -0
  3. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/pyproject.toml +8 -1
  4. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/__init__.py +1 -1
  5. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/PKG-INFO +14 -1
  6. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/SOURCES.txt +1 -0
  7. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/requires.txt +6 -0
  8. smartcli_toolkit-0.1.4/tests/test_golden_frames.py +161 -0
  9. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/LICENSE +0 -0
  10. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/setup.cfg +0 -0
  11. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/pty_backend.py +0 -0
  12. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/py.typed +0 -0
  13. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/readiness.py +0 -0
  14. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/screen_model.py +0 -0
  15. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/session.py +0 -0
  16. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_core/snapshot.py +0 -0
  17. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/dependency_links.txt +0 -0
  18. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/smartcli_toolkit.egg-info/top_level.txt +0 -0
  19. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_degenerate_inputs.py +0 -0
  20. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_doc_counts.py +0 -0
  21. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_fx_contract.py +0 -0
  22. {smartcli_toolkit-0.1.3 → smartcli_toolkit-0.1.4}/tests/test_readiness.py +0 -0
  23. {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
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
  [![PyPI](https://img.shields.io/pypi/v/smartcli-toolkit?color=orange)](https://pypi.org/project/smartcli-toolkit/)
52
56
  [![Python](https://img.shields.io/pypi/pyversions/smartcli-toolkit?color=blue)](https://pypi.org/project/smartcli-toolkit/)
53
57
  [![CI](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml/badge.svg)](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
58
+ [![codecov](https://codecov.io/gh/dwgx/SmartCLI/branch/main/graph/badge.svg)](https://codecov.io/gh/dwgx/SmartCLI)
54
59
  [![License: MIT](https://img.shields.io/pypi/l/smartcli-toolkit?color=green)](LICENSE)
55
60
  [![Downloads](https://img.shields.io/pypi/dm/smartcli-toolkit?color=blueviolet)](https://pypi.org/project/smartcli-toolkit/)
56
61
  [![Skills: 3](https://img.shields.io/badge/skills-3-purple)](#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
  [![PyPI](https://img.shields.io/pypi/v/smartcli-toolkit?color=orange)](https://pypi.org/project/smartcli-toolkit/)
10
10
  [![Python](https://img.shields.io/pypi/pyversions/smartcli-toolkit?color=blue)](https://pypi.org/project/smartcli-toolkit/)
11
11
  [![CI](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml/badge.svg)](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
12
+ [![codecov](https://codecov.io/gh/dwgx/SmartCLI/branch/main/graph/badge.svg)](https://codecov.io/gh/dwgx/SmartCLI)
12
13
  [![License: MIT](https://img.shields.io/pypi/l/smartcli-toolkit?color=green)](LICENSE)
13
14
  [![Downloads](https://img.shields.io/pypi/dm/smartcli-toolkit?color=blueviolet)](https://pypi.org/project/smartcli-toolkit/)
14
15
  [![Skills: 3](https://img.shields.io/badge/skills-3-purple)](#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.3"
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).
@@ -55,4 +55,4 @@ __all__ = [
55
55
  "KEY_MAP",
56
56
  ]
57
57
 
58
- __version__ = "0.1.3"
58
+ __version__ = "0.1.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartcli-toolkit
3
- Version: 0.1.3
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
  [![PyPI](https://img.shields.io/pypi/v/smartcli-toolkit?color=orange)](https://pypi.org/project/smartcli-toolkit/)
52
56
  [![Python](https://img.shields.io/pypi/pyversions/smartcli-toolkit?color=blue)](https://pypi.org/project/smartcli-toolkit/)
53
57
  [![CI](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml/badge.svg)](https://github.com/dwgx/SmartCLI/actions/workflows/ci.yml)
58
+ [![codecov](https://codecov.io/gh/dwgx/SmartCLI/branch/main/graph/badge.svg)](https://codecov.io/gh/dwgx/SmartCLI)
54
59
  [![License: MIT](https://img.shields.io/pypi/l/smartcli-toolkit?color=green)](LICENSE)
55
60
  [![Downloads](https://img.shields.io/pypi/dm/smartcli-toolkit?color=blueviolet)](https://pypi.org/project/smartcli-toolkit/)
56
61
  [![Skills: 3](https://img.shields.io/badge/skills-3-purple)](#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:
@@ -16,5 +16,6 @@ smartcli_toolkit.egg-info/top_level.txt
16
16
  tests/test_degenerate_inputs.py
17
17
  tests/test_doc_counts.py
18
18
  tests/test_fx_contract.py
19
+ tests/test_golden_frames.py
19
20
  tests/test_readiness.py
20
21
  tests/test_vendor_sync.py
@@ -11,8 +11,14 @@ wcwidth>=0.2.0
11
11
  [art]
12
12
  pyfiglet>=1.0.0
13
13
 
14
+ [dev]
15
+ coverage[toml]>=7.0
16
+
14
17
  [image]
15
18
  Pillow>=10.0.0
16
19
 
20
+ [mcp]
21
+ mcp>=1.0
22
+
17
23
  [width]
18
24
  wcwidth>=0.2.0
@@ -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:]))