hexastack-cli 0.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.
@@ -0,0 +1,192 @@
1
+ """Terminal session testing and feature demo narration engine for CLI applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from typer.testing import CliRunner
12
+
13
+
14
+ @dataclass
15
+ class TerminalEvent:
16
+ """Represents a discrete timestamped event in a recorded terminal session."""
17
+
18
+ time_offset: float
19
+ event_type: str # "input", "output", "step"
20
+ payload: str
21
+
22
+
23
+ class CliNarrator:
24
+ """Orchestrates CLI command execution, human-like typing emulation, and session recording.
25
+
26
+ Notes/Architectural Intent:
27
+ Provides a unified interface for testing CLI tools that:
28
+ 1. In standard test / CI mode: Runs instantly via in-memory CliRunner.
29
+ 2. In demo mode (RECORD_DEMO=1): Generates WebVTT narration subtitles
30
+ and directly renders a fixed-window, auto-scrolling .webm video via Playwright.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ app: Any,
36
+ output_name: str | None = None,
37
+ output_dir: Path | None = None,
38
+ width: int = 100,
39
+ height: int = 24,
40
+ ) -> None:
41
+ """Initialize CLI narrator attached to a Typer or Click application.
42
+
43
+ Args:
44
+ app: Target Typer application instance.
45
+ output_name: Base filename (without extension) for saving recordings.
46
+ output_dir: Target output directory for generated demo artifacts.
47
+ width: Terminal column width.
48
+ height: Terminal row height.
49
+ """
50
+ self.app = app
51
+ self.output_name = output_name
52
+ self.output_dir = output_dir or Path("docs/assets/demos")
53
+ self.width = width
54
+ self.height = height
55
+ self.record_mode = os.environ.get("RECORD_DEMO") in ("1", "true", "True")
56
+ self.runner = CliRunner()
57
+ self.start_time: float = time.time()
58
+ self.events: list[TerminalEvent] = []
59
+ self.captions: list[tuple[float, float, str]] = []
60
+ self._current_step_start: float | None = None
61
+ self._current_step_text: str | None = None
62
+
63
+ def step(self, caption: str) -> None:
64
+ """Add a descriptive chapter step / subtitle narration for viewers.
65
+
66
+ Args:
67
+ caption: Subtitle narration text explaining the current terminal action.
68
+ """
69
+ now = time.time() - self.start_time
70
+
71
+ if self._current_step_start is not None and self._current_step_text is not None:
72
+ self.captions.append(
73
+ (self._current_step_start, now, self._current_step_text)
74
+ )
75
+
76
+ self._current_step_start = now
77
+ self._current_step_text = caption
78
+ self.events.append(
79
+ TerminalEvent(time_offset=now, event_type="step", payload=caption)
80
+ )
81
+
82
+ def run_command(
83
+ self,
84
+ args: list[str],
85
+ caption: str | None = None,
86
+ input_text: str | None = None,
87
+ type_delay: float = 0.05,
88
+ ) -> Any:
89
+ """Execute a CLI command with simulated typing cadence and output recording.
90
+
91
+ Args:
92
+ args: Command line arguments list (e.g. ["new", "web-api", "my-service"]).
93
+ caption: Optional narration step explaining this command.
94
+ input_text: Optional interactive stdin text to feed to the command.
95
+ type_delay: Seconds per keystroke when recording in demo mode.
96
+
97
+ Returns:
98
+ CliRunner invocation result.
99
+ """
100
+ if caption:
101
+ self.step(caption)
102
+
103
+ cmd_str = "hexastack " + " ".join(args)
104
+ cmd_start = time.time() - self.start_time
105
+
106
+ if self.record_mode:
107
+ # Simulate human keystroke cadence in event timeline
108
+ sim_time = cmd_start
109
+ for char in f"$ {cmd_str}\n":
110
+ sim_time += type_delay
111
+ self.events.append(
112
+ TerminalEvent(
113
+ time_offset=sim_time, event_type="input", payload=char
114
+ )
115
+ )
116
+
117
+ # Execute command synchronously
118
+ result = self.runner.invoke(self.app, args, input=input_text)
119
+ out_time = time.time() - self.start_time
120
+
121
+ self.events.append(
122
+ TerminalEvent(
123
+ time_offset=out_time, event_type="output", payload=result.output
124
+ )
125
+ )
126
+
127
+ return result
128
+
129
+ def finish(self) -> dict[str, Path]:
130
+ """Finalize recording and export .webm and .vtt subtitle artifacts.
131
+
132
+ Returns:
133
+ Dictionary containing written artifact paths.
134
+ """
135
+ if not self.record_mode:
136
+ return {}
137
+
138
+ now = time.time() - self.start_time
139
+ if self._current_step_start is not None and self._current_step_text is not None:
140
+ self.captions.append(
141
+ (self._current_step_start, now, self._current_step_text)
142
+ )
143
+
144
+ out_dir = self.output_dir
145
+ out_dir.mkdir(parents=True, exist_ok=True)
146
+ base_name = self.output_name or "cli-demo"
147
+ artifacts: dict[str, Path] = {}
148
+
149
+ # 1. Export WebVTT (.vtt) synchronized narration subtitles
150
+ vtt_lines = ["WEBVTT", ""]
151
+ for idx, (start_s, end_s, text) in enumerate(self.captions, start=1):
152
+ vtt_lines.append(str(idx))
153
+ vtt_lines.append(
154
+ f"{self._format_vtt_time(start_s)} --> {self._format_vtt_time(end_s)}"
155
+ )
156
+ vtt_lines.append(text)
157
+ vtt_lines.append("")
158
+
159
+ vtt_path = out_dir / f"{base_name}.vtt"
160
+ vtt_path.write_text("\n".join(vtt_lines), encoding="utf-8")
161
+ artifacts["vtt"] = vtt_path
162
+
163
+ # 2. Render directly to .webm video via Playwright Rich Terminal
164
+ try:
165
+ from hexastack_cli.testing.terminal import render_cli_demo_video
166
+
167
+ video_path = out_dir / f"{base_name}.webm"
168
+ render_cli_demo_video(
169
+ events=self.events,
170
+ output_path=video_path,
171
+ title=f"Hexastack CLI — {base_name}",
172
+ )
173
+ artifacts["webm"] = video_path
174
+ except Exception:
175
+ pass
176
+
177
+ return artifacts
178
+
179
+ @staticmethod
180
+ def _format_vtt_time(seconds: float) -> str:
181
+ """Format seconds into WebVTT timestamp (HH:MM:SS.mmm)."""
182
+ hrs = int(seconds // 3600)
183
+ mins = int((seconds % 3600) // 60)
184
+ secs = int(seconds % 60)
185
+ millis = int(round((seconds - int(seconds)) * 1000))
186
+ return f"{hrs:02d}:{mins:02d}:{secs:02d}.{millis:03d}"
187
+
188
+
189
+ __all__ = [
190
+ "CliNarrator",
191
+ "TerminalEvent",
192
+ ]
@@ -0,0 +1,298 @@
1
+ """Rich HTML Terminal view and Playwright video renderer for CLI demo recordings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from hexastack_cli.testing.narrator import TerminalEvent
11
+ from hexastack_core.domain.exceptions import MissingDependencyError
12
+
13
+ TERMINAL_HTML_TEMPLATE = """<!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <title>{title}</title>
18
+ <style>
19
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
20
+ body {{
21
+ background: #0d1117;
22
+ color: #e6edf3;
23
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ height: 100vh;
28
+ overflow: hidden;
29
+ padding: 40px;
30
+ }}
31
+ .window {{
32
+ background: #161b22;
33
+ border-radius: 12px;
34
+ box-shadow: 0 20px 50px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1);
35
+ width: 1080px;
36
+ height: 560px;
37
+ max-height: 560px;
38
+ display: flex;
39
+ flex-direction: column;
40
+ overflow: hidden;
41
+ }}
42
+ .titlebar {{
43
+ background: #21262d;
44
+ padding: 12px 16px;
45
+ display: flex;
46
+ align-items: center;
47
+ border-bottom: 1px solid rgba(255,255,255,0.08);
48
+ flex-shrink: 0;
49
+ }}
50
+ .dots {{
51
+ display: flex;
52
+ gap: 8px;
53
+ }}
54
+ .dot {{
55
+ width: 12px;
56
+ height: 12px;
57
+ border-radius: 50%;
58
+ }}
59
+ .dot-red {{ background: #ff5f56; }}
60
+ .dot-yellow {{ background: #ffbd2e; }}
61
+ .dot-green {{ background: #27c93f; }}
62
+ .title {{
63
+ flex: 1;
64
+ text-align: center;
65
+ color: #8b949e;
66
+ font-size: 13px;
67
+ font-weight: 500;
68
+ margin-right: 48px;
69
+ }}
70
+ .content {{
71
+ padding: 24px;
72
+ font-size: 15px;
73
+ line-height: 1.6;
74
+ flex: 1;
75
+ overflow-y: auto;
76
+ white-space: pre-wrap;
77
+ word-break: break-word;
78
+ scroll-behavior: smooth;
79
+ }}
80
+ .content::-webkit-scrollbar {{
81
+ display: none;
82
+ }}
83
+ .content {{
84
+ -ms-overflow-style: none;
85
+ scrollbar-width: none;
86
+ }}
87
+ .prompt {{ color: #7ee787; font-weight: bold; }}
88
+ .command {{ color: #79c0ff; font-weight: bold; }}
89
+ .step-banner {{
90
+ position: fixed;
91
+ bottom: 32px;
92
+ left: 50%;
93
+ transform: translateX(-50%);
94
+ background: rgba(15, 23, 42, 0.92);
95
+ color: #ffffff;
96
+ padding: 12px 24px;
97
+ border-radius: 8px;
98
+ font-size: 17px;
99
+ font-family: system-ui, -apple-system, sans-serif;
100
+ font-weight: 500;
101
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
102
+ border: 1px solid rgba(255,255,255,0.15);
103
+ backdrop-filter: blur(8px);
104
+ z-index: 9999;
105
+ transition: opacity 0.2s ease-in-out;
106
+ text-align: center;
107
+ max-width: 80%;
108
+ }}
109
+ .cursor {{
110
+ display: inline-block;
111
+ width: 8px;
112
+ height: 16px;
113
+ background: #58a6ff;
114
+ vertical-align: text-bottom;
115
+ animation: blink 1s step-end infinite;
116
+ }}
117
+ @keyframes blink {{ 50% {{ opacity: 0; }} }}
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div class="window">
122
+ <div class="titlebar">
123
+ <div class="dots">
124
+ <div class="dot dot-red"></div>
125
+ <div class="dot dot-yellow"></div>
126
+ <div class="dot dot-green"></div>
127
+ </div>
128
+ <div class="title">{title} — bash</div>
129
+ </div>
130
+ <div class="content" id="terminal-content"><span class="cursor"></span></div>
131
+ </div>
132
+ <div class="step-banner" id="banner" style="display: none;"></div>
133
+ </body>
134
+ </html>
135
+ """
136
+
137
+
138
+ def _strip_ansi(text: str) -> str:
139
+ """Remove ANSI color and escape sequences from text."""
140
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
141
+ return ansi_escape.sub("", text)
142
+
143
+
144
+ def render_cli_demo_video(
145
+ events: list[TerminalEvent],
146
+ output_path: Path,
147
+ title: str = "Hexastack CLI",
148
+ width: int = 1280,
149
+ height: int = 720,
150
+ ) -> tuple[Path, Path]:
151
+ """Render a sequence of TerminalEvents into synchronized .webm video and .vtt subtitle track.
152
+
153
+ Args:
154
+ events: Chronological sequence of input, output, and step narrative events.
155
+ output_path: Destination Path for .webm video file.
156
+ title: Window title text for terminal banner.
157
+ width: Video frame width in pixels.
158
+ height: Video frame height in pixels.
159
+
160
+ Returns:
161
+ Tuple of (video_path, vtt_path).
162
+
163
+ Raises:
164
+ MissingDependencyError: If `playwright` is not installed.
165
+ """
166
+ try:
167
+ from playwright.sync_api import sync_playwright
168
+ except ImportError as e:
169
+ raise MissingDependencyError(
170
+ "Playwright is required to record CLI demos into .webm video. "
171
+ "Install with 'pip install hexastack-cli[testing]' or 'pip install playwright'."
172
+ ) from e
173
+
174
+ output_path.parent.mkdir(parents=True, exist_ok=True)
175
+ temp_dir = output_path.parent / ".temp_video"
176
+ temp_dir.mkdir(parents=True, exist_ok=True)
177
+
178
+ html_content = TERMINAL_HTML_TEMPLATE.format(title=html.escape(title))
179
+ vtt_captions: list[tuple[float, float, str]] = []
180
+ current_step_start: float | None = None
181
+ current_step_text: str | None = None
182
+
183
+ with sync_playwright() as p:
184
+ browser = p.chromium.launch(headless=True)
185
+ context = browser.new_context(
186
+ viewport={"width": width, "height": height},
187
+ record_video_dir=str(temp_dir),
188
+ record_video_size={"width": width, "height": height},
189
+ )
190
+ page = context.new_page()
191
+ page.set_content(html_content)
192
+ page.wait_for_timeout(500)
193
+ start_playback_time = 0.5
194
+
195
+ current_time = start_playback_time
196
+
197
+ for ev in events:
198
+ if ev.event_type == "step":
199
+ if current_step_start is not None and current_step_text is not None:
200
+ vtt_captions.append(
201
+ (current_step_start, current_time, current_step_text)
202
+ )
203
+
204
+ current_step_start = current_time
205
+ current_step_text = ev.payload
206
+
207
+ safe_text = html.escape(ev.payload).replace("'", "\\'")
208
+ page.evaluate(
209
+ f"""(() => {{
210
+ const banner = document.getElementById('banner');
211
+ if (banner) {{
212
+ banner.innerText = '{safe_text}';
213
+ banner.style.display = 'block';
214
+ }}
215
+ }})();"""
216
+ )
217
+ page.wait_for_timeout(600)
218
+ current_time += 0.6
219
+
220
+ elif ev.event_type == "input":
221
+ char_str = html.escape(ev.payload)
222
+ if char_str == "\n":
223
+ char_str = "<br/>"
224
+ page.evaluate(
225
+ f"""(() => {{
226
+ const terminal = document.getElementById('terminal-content');
227
+ const cursor = terminal.querySelector('.cursor');
228
+ const span = document.createElement('span');
229
+ span.className = 'command';
230
+ span.innerHTML = '{char_str}';
231
+ terminal.insertBefore(span, cursor);
232
+ terminal.scrollTop = terminal.scrollHeight;
233
+ }})();"""
234
+ )
235
+ page.wait_for_timeout(35)
236
+ current_time += 0.035
237
+
238
+ elif ev.event_type == "output":
239
+ clean_text = _strip_ansi(ev.payload)
240
+ escaped_output = html.escape(clean_text).replace("\n", "<br/>")
241
+ page.evaluate(
242
+ f"""(() => {{
243
+ const terminal = document.getElementById('terminal-content');
244
+ const cursor = terminal.querySelector('.cursor');
245
+ const div = document.createElement('div');
246
+ div.style.color = '#c9d1d9';
247
+ div.style.margin = '4px 0 12px 0';
248
+ div.innerHTML = '{escaped_output}';
249
+ terminal.insertBefore(div, cursor);
250
+ terminal.scrollTop = terminal.scrollHeight;
251
+ }})();"""
252
+ )
253
+ page.wait_for_timeout(1000)
254
+ current_time += 1.0
255
+
256
+ if current_step_start is not None and current_step_text is not None:
257
+ vtt_captions.append(
258
+ (current_step_start, current_time + 1.5, current_step_text)
259
+ )
260
+
261
+ page.wait_for_timeout(1500)
262
+ video = page.video
263
+ page.close()
264
+ context.close()
265
+ browser.close()
266
+
267
+ if video:
268
+ video_src = Path(video.path())
269
+ shutil.copy2(video_src, output_path)
270
+
271
+ shutil.rmtree(temp_dir, ignore_errors=True)
272
+
273
+ # Export synchronized WebVTT subtitles with true video timestamps
274
+ vtt_lines = ["WEBVTT", ""]
275
+ for idx, (start_s, end_s, text) in enumerate(vtt_captions, start=1):
276
+ vtt_lines.append(str(idx))
277
+ vtt_lines.append(f"{_format_vtt_time(start_s)} --> {_format_vtt_time(end_s)}")
278
+ vtt_lines.append(text)
279
+ vtt_lines.append("")
280
+
281
+ vtt_path = output_path.with_suffix(".vtt")
282
+ vtt_path.write_text("\n".join(vtt_lines), encoding="utf-8")
283
+
284
+ return output_path, vtt_path
285
+
286
+
287
+ def _format_vtt_time(seconds: float) -> str:
288
+ """Format seconds into WebVTT timestamp (HH:MM:SS.mmm)."""
289
+ hrs = int(seconds // 3600)
290
+ mins = int((seconds % 3600) // 60)
291
+ secs = int(seconds % 60)
292
+ millis = int(round((seconds - int(seconds)) * 1000))
293
+ return f"{hrs:02d}:{mins:02d}:{secs:02d}.{millis:03d}"
294
+
295
+
296
+ __all__ = [
297
+ "render_cli_demo_video",
298
+ ]
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.3
2
+ Name: hexastack-cli
3
+ Version: 0.0.0
4
+ Summary: Hexastack CLI presentation adapter with Typer and Rich
5
+ Author: Richard West
6
+ Author-email: Richard West <dopplereffect.us@gmail.com>
7
+ Requires-Dist: hexastack-core
8
+ Requires-Dist: hexastack-cqrs
9
+ Requires-Dist: rich>=13.8.0
10
+ Requires-Dist: typer>=0.27.1
11
+ Requires-Dist: playwright>=1.49.0 ; extra == 'testing'
12
+ Requires-Python: >=3.13
13
+ Provides-Extra: testing
14
+ Description-Content-Type: text/markdown
15
+
16
+ # hexastack-cli
17
+
18
+ > Typer and Rich presentation adapter for Hexastack: nested commands, aliases, piped outputs, and CQRS dispatching.
19
+
20
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
21
+
22
+ ---
23
+
24
+ ## 1. Overview & Capabilities
25
+
26
+ `hexastack-cli` turns Hexastack CQRS commands and queries into intuitive, modern CLI applications:
27
+
28
+ - **Nested Command Hierarchies**: Nest subcommands naturally (`app user create`, `app db migrate`) using `@cli_group`.
29
+ - **Command Aliases**: Register multiple aliases for the same action (`app user new == app user create`).
30
+ - **Feature Flag Gating**: Gate CLI commands dynamically with `@feature_flag_command(...)` and `@cli_command(..., feature_flag=...)`.
31
+ - **Rich Formatted & CI-Friendly Output**: Beautiful tables, panels, and spinners for interactive terminals; clean text/JSON streaming for CI/CD pipelines.
32
+ - **Direct CQRS Dispatching**: Declaratively expose domain commands (`@cli_command`) and queries (`@cli_query`) with automatic parameter parsing and validation.
33
+
34
+ ---
35
+
36
+ ## 2. Package Anatomy & Key Components
37
+
38
+ ```
39
+ hexastack_cli/
40
+ ├── domain/ # CliContext, OutputFormat enum
41
+ ├── adapters/ # create_cli_app, Rich presenters, Typer command runners
42
+ └── infra/ # CliBootstrapper (order=30), @cli_command, @cli_query, @cli_group, @feature_flag_command
43
+ ```
44
+
45
+ ### Key Exports
46
+
47
+ | Category | Exports |
48
+ |---|---|
49
+ | **Application Factory** | `create_cli_app`, `CliBootstrapper` (order=30) |
50
+ | **Decorators** | `@cli_command`, `@cli_query`, `@cli_group`, `@feature_flag_command` |
51
+ | **Presenters** | `RichTerminalPresenter`, `ConsolePresenter`, `TablePresenter`, `JsonPresenter` |
52
+ | **Testing & Demo Narration** | `CliNarrator`, `TerminalEvent` |
53
+
54
+ ---
55
+
56
+ ## 3. Monorepo & Sibling Relationships
57
+
58
+ ```mermaid
59
+ graph TD
60
+ subgraph UserInvocation ["CLI Invocations"]
61
+ INV["Terminal Commands & Scripts"]
62
+ end
63
+
64
+ subgraph CliLayer ["hexastack-cli"]
65
+ TYPER["Typer Application (Nested Groups & Aliases)"]
66
+ RICH["Rich Presenters (Tables, Panels, JSON)"]
67
+ SCAN["CLI Decorator Scanner (@cli_command, @cli_query)"]
68
+ end
69
+
70
+ subgraph CQRSExecution ["hexastack-cqrs"]
71
+ CBUS["CommandBusPort"]
72
+ QBUS["QueryBusPort"]
73
+ end
74
+
75
+ subgraph Kernel ["hexastack-core"]
76
+ DI["rodi.Container"]
77
+ end
78
+
79
+ INV --> TYPER
80
+ TYPER --> SCAN
81
+ SCAN -->|dispatches to| CBUS
82
+ SCAN -->|dispatches to| QBUS
83
+ SCAN --> RICH
84
+
85
+ TYPER -. resolves buses from DI .-> DI
86
+ ```
87
+
88
+ ### Explicit Dependencies (Direct)
89
+ - `hexastack-core`: Core kernel, DI container, and ports.
90
+ - `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
91
+ - `typer>=0.27.1`: CLI command parser and shell completion.
92
+ - `rich>=15.0.0`: Terminal formatting, tables, and colors.
93
+
94
+ ### Implied / Behavioral Relationships (DI-Mediated)
95
+ - **CQRS Integration**: Dispatches CLI argument payloads directly into the application's command and query buses.
96
+ - **Umbrella CLI**: Consumed by the `hexastack` umbrella package to power diagnostic commands (`hexastack info`, `hexastack inspect registry`, `hexastack demo ping`).
97
+
98
+ ---
99
+
100
+ ## 4. Installation
101
+
102
+ ```bash
103
+ # Standalone install
104
+ pip install hexastack-cli
105
+
106
+ # Via umbrella package
107
+ pip install "hexastack[cli]"
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 5. Quickstart Example
113
+
114
+ ```python
115
+ from dataclasses import dataclass
116
+ from hexastack_core.infra.bootstrap import bootstrap
117
+ from hexastack_cqrs.domain.query import Query
118
+ from hexastack_cqrs.infra.decorators import query_handler
119
+ from hexastack_cli.infra.decorators import cli_query, cli_group
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class CheckStatusQuery(Query):
124
+ service_name: str
125
+
126
+
127
+ @query_handler(CheckStatusQuery)
128
+ class CheckStatusHandler:
129
+ def __call__(self, qry: CheckStatusQuery) -> dict:
130
+ return {"service": qry.service_name, "status": "ONLINE"}
131
+
132
+
133
+ # Expose query as a CLI command
134
+ cli_query("status", aliases=["st", "health"], help="Check status of a service")(
135
+ CheckStatusQuery
136
+ )
137
+
138
+ runtime = bootstrap(packages_to_scan=[__name__])
139
+ cli_app = runtime.get("cli_app")
140
+ ```
@@ -0,0 +1,18 @@
1
+ hexastack_cli/__init__.py,sha256=BxoY0Y5o6VtjzlPkspHmgZYaPuFAKVmA5hwnpg93CMk,86
2
+ hexastack_cli/adapters/__init__.py,sha256=JzoxVVKhUjuRmAJO5MK0mrwLwTKXl9xO7zLfohk22Cw,342
3
+ hexastack_cli/adapters/app.py,sha256=BuS5pjqlg3Fv_6K90TFvADXtwfDfVSak8R-2oIzKJPg,2576
4
+ hexastack_cli/adapters/presenter.py,sha256=3TRwSSxLgEhFVvcDRu5VogHoC-kVAgdHDEAD3p4u9vo,4419
5
+ hexastack_cli/adapters/routing.py,sha256=v02J5KpyqYuAEdjG38Gr-pxLxloWQle3oa-DZnObbLk,13394
6
+ hexastack_cli/infra/__init__.py,sha256=m4BGkzq6_9zbmhBn3Qwvxtp56J7-i1haflleg5vauKA,628
7
+ hexastack_cli/infra/autodiscovery.py,sha256=Mr-YLrEaf38_zQcOgjVJlDtLbX7bt-bjQODbxW5X0ng,6848
8
+ hexastack_cli/infra/bootstrap.py,sha256=99tD1hdnNWRDyLNwQJ9ogBdJgpGIV9OPzDFVde_d_Q4,2475
9
+ hexastack_cli/infra/config.py,sha256=e79HN42O_fnBvsHEKhsET5deWHAZrPJPABmMQrO9940,1235
10
+ hexastack_cli/infra/decorators.py,sha256=fmU2Svga9GdaoOyVZLXOrUCCtbdmVbLq0gISrwcph30,7167
11
+ hexastack_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hexastack_cli/testing/__init__.py,sha256=zB7VmUX7T0Hpuyv9O1Qfj4PkgfuE06DLklnahkWl1LQ,295
13
+ hexastack_cli/testing/narrator.py,sha256=dXInuCxPzUFSyo0o0ME9x9xRYsRvBHdNbMv3OCOanS8,6542
14
+ hexastack_cli/testing/terminal.py,sha256=QOHhLtRw6bk27gKji5DDNQKCb0sUxwVMpSxIEXpU0EE,10248
15
+ hexastack_cli-0.0.0.dist-info/WHEEL,sha256=EmLkUISDECbcUx3FMCYOqokNOJqNp2r0d4mJzjErvvs,80
16
+ hexastack_cli-0.0.0.dist-info/entry_points.txt,sha256=LPpUygBzbXJ0_5IXxaesAbJjJ5Vmp8wQQOtgfbY5Yj8,79
17
+ hexastack_cli-0.0.0.dist-info/METADATA,sha256=Id59pQZbPtZ_Ya4igL7J1vSjOqR26y_JnJR3Jz1kEqg,4529
18
+ hexastack_cli-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [hexastack.bootstrappers]
2
+ cli = hexastack_cli.infra.bootstrap:CliBootstrapper
3
+