pywire-cli 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.
pywire_cli/tui.py ADDED
@@ -0,0 +1,574 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ import time
5
+ from textual.app import App, ComposeResult
6
+ from textual.coordinate import Coordinate
7
+ from textual.widgets import Header, Footer, Label, DataTable
8
+ from textual.binding import Binding
9
+ from rich.text import Text
10
+ import shutil
11
+ import subprocess
12
+ from textual import events
13
+ from typing import cast, TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ # Use forward references for types defined in this file
17
+ pass
18
+
19
+
20
+ class LogTable(DataTable):
21
+ async def on_mouse_down(self, event: events.MouseDown) -> None:
22
+ if self.app and hasattr(self.app, "handle_log_mouse_down"):
23
+ try:
24
+ coord = Coordinate(event.offset.x, event.offset.y)
25
+ meta = self.get_cell_at(coord)
26
+ if meta:
27
+ row_key = self.coordinate_to_cell_key(meta).row_key
28
+ if row_key and row_key.value:
29
+ app = cast("PyWireDevDashboard", self.app)
30
+ if app.handle_log_mouse_down(row_key.value, shift=event.shift):
31
+ self.capture_mouse()
32
+ self._mouse_captured = True
33
+ except Exception:
34
+ pass
35
+
36
+ async def on_mouse_move(self, event: events.MouseMove) -> None:
37
+ if (
38
+ getattr(self, "_mouse_captured", False)
39
+ and self.app
40
+ and hasattr(self.app, "handle_log_mouse_move")
41
+ ):
42
+ try:
43
+ coord = Coordinate(event.offset.x, event.offset.y)
44
+ meta = self.get_cell_at(coord)
45
+ if meta:
46
+ row_key = self.coordinate_to_cell_key(meta).row_key
47
+ if row_key and row_key.value:
48
+ app = cast("PyWireDevDashboard", self.app)
49
+ app.handle_log_mouse_move(row_key.value)
50
+ except Exception:
51
+ pass
52
+
53
+ async def on_mouse_up(self, event: events.MouseUp) -> None:
54
+ if getattr(self, "_mouse_captured", False):
55
+ self.release_mouse()
56
+ self._mouse_captured = False
57
+
58
+ def on_click(self, event: events.Click) -> None:
59
+ # We handle click logic mostly in mouse_down for drag start,
60
+ # but simple toggle/click might need to be resolved here if not dragging?
61
+ # Actually, mouse_down starts a potential drag.
62
+ # If we release on same cell without moving much, it's a click.
63
+ # But we can just use mouse_down to "start selection" (select 1 cell).
64
+ # And mouse_move to "extend".
65
+ # So on_click is less needed if we handle mouse_down?
66
+ # But super().on_click handles row cursor activation.
67
+ # We'll pass through.
68
+ # super().on_click(event) - AttributeError: 'super' object has no attribute 'on_click'
69
+ pass
70
+
71
+
72
+ class PyWireDevDashboard(App):
73
+ CSS = """
74
+ Screen { layout: vertical; }
75
+ DataTable { width: 100%; height: 1fr; border: solid $accent; }
76
+ DataTable > .datatable--cursor { background: $accent 20%; }
77
+ DataTable > .datatable--header { display: none; }
78
+ #header-info { dock: top; height: 1; content-align: center middle; background: $primary; color: $text; }
79
+ """
80
+
81
+ BINDINGS = [
82
+ Binding("q", "quit", "Quit"),
83
+ Binding("r", "restart_server", "Restart Server"),
84
+ Binding("c", "clear_logs", "Clear Logs"),
85
+ Binding("l", "toggle_log_level", "Toggle Log Level"),
86
+ Binding("y", "copy_logs", "Copy Logs (Clipboard)"),
87
+ Binding("space", "toggle_selection", "Select/Deselect Line"),
88
+ Binding("enter", "toggle_selection", "Select/Deselect Line"),
89
+ Binding("escape", "deselect_all", "Deselect All"),
90
+ ]
91
+
92
+ def __init__(self, command: list[str], host: str, port: int):
93
+ super().__init__()
94
+ self.command = command
95
+ self.host = host
96
+ self.port = port
97
+ self.server_process: asyncio.subprocess.Process | None = None
98
+ self.start_time = time.time()
99
+ # User requested: Debug -> Info -> Warning -> Error, starting at Info
100
+ self.log_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
101
+ self.current_log_level_index = 1 # Start at INFO
102
+ self._log_store: list[tuple[Text, int]] = []
103
+ self._selected_indices: set[int] = set()
104
+ self._last_selected_index: int | None = None
105
+ self._drag_start_index: int | None = None
106
+ self._is_dragging: bool = False
107
+
108
+ @property
109
+ def current_log_level(self) -> str:
110
+ return self.log_levels[self.current_log_level_index]
111
+
112
+ def compose(self) -> ComposeResult:
113
+ self.title = "pywire Dev Dashboard"
114
+ yield Header(show_clock=False)
115
+ yield Label(
116
+ f"Server: http://{self.host}:{self.port} | Uptime: 00:00:00",
117
+ id="header-info",
118
+ )
119
+
120
+ # Using DataTable for selectable log rows
121
+ # Using LogTable for selectable log rows (supports shift+click)
122
+ table = LogTable(id="log-window", cursor_type="row", zebra_stripes=False)
123
+ table.add_column("Log", key="log")
124
+ yield table
125
+
126
+ yield Footer()
127
+
128
+ async def on_mount(self) -> None:
129
+ """Start the server when the TUI loads."""
130
+ protocol = "https" if "--ssl-keyfile" in self.command else "http"
131
+ self.log_write(
132
+ f"[bold yellow]Initializing pywire Server on {protocol}://{self.host}:{self.port}...[/]",
133
+ level=20,
134
+ )
135
+
136
+ # Start server task
137
+ self.server_task = asyncio.create_task(self.run_server())
138
+ self.set_interval(1, self.update_uptime)
139
+
140
+ async def on_unmount(self) -> None:
141
+ """Ensure server subprocess is killed when TUI exits."""
142
+ # Cancel the server loop task
143
+ if hasattr(self, "server_task"):
144
+ self.server_task.cancel()
145
+ try:
146
+ await self.server_task
147
+ except asyncio.CancelledError:
148
+ pass
149
+
150
+ if self.server_process and self.server_process.returncode is None:
151
+ try:
152
+ self.server_process.terminate()
153
+ # Give it 1 second to die gracefully
154
+ try:
155
+ await asyncio.wait_for(self.server_process.wait(), timeout=1.0)
156
+ except asyncio.TimeoutError:
157
+ self.server_process.kill()
158
+ await self.server_process.wait()
159
+ except ProcessLookupError:
160
+ pass
161
+ except Exception:
162
+ try:
163
+ self.server_process.kill()
164
+ except Exception:
165
+ pass
166
+
167
+ def update_uptime(self) -> None:
168
+ uptime_seconds = int(time.time() - self.start_time)
169
+ hours, remainder = divmod(uptime_seconds, 3600)
170
+ minutes, seconds = divmod(remainder, 60)
171
+ uptime_str = f"{hours:02}:{minutes:02}:{seconds:02}"
172
+
173
+ protocol = "https" if "--ssl-keyfile" in self.command else "http"
174
+
175
+ header_info = self.query_one("#header-info", Label)
176
+ header_info.update(
177
+ f"Server: {protocol}://{self.host}:{self.port} | Uptime: {uptime_str} | Log Level: {self.current_log_level}"
178
+ )
179
+
180
+ def log_write(self, message: str | Text, level: int = 20) -> None:
181
+ """Writes a message to the internal log store and updates the widget if visible."""
182
+ if isinstance(message, str):
183
+ text = Text.from_markup(message)
184
+ else:
185
+ text = message
186
+
187
+ entry = (text, level)
188
+ self._log_store.append(entry)
189
+
190
+ level_map = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40}
191
+ current_threshold = level_map.get(self.current_log_level, 20)
192
+
193
+ if level >= current_threshold:
194
+ self._add_log_to_table(text, len(self._log_store) - 1)
195
+
196
+ def _add_log_to_table(self, text: Text, store_index: int):
197
+ """Helper to add a row to the table safely."""
198
+ try:
199
+ table = self.query_one("#log-window", DataTable)
200
+
201
+ # Check if this index is selected
202
+ if store_index in self._selected_indices:
203
+ # User requested NO caret, just highlight
204
+ display_text = text
205
+ # Use a specific high-contrast style for selection
206
+ display_text.style = "bold white on $secondary"
207
+ else:
208
+ display_text = text
209
+
210
+ table.add_row(display_text, key=str(store_index))
211
+
212
+ # Auto-scroll to bottom
213
+ table.move_cursor(row=table.row_count - 1, animate=False)
214
+ except Exception:
215
+ # Widget might be unmounted or not found
216
+ pass
217
+
218
+ def refresh_log_view(self):
219
+ """Clears and repopulates the log window based on current filter."""
220
+ try:
221
+ table = self.query_one("#log-window", DataTable)
222
+ table.clear()
223
+
224
+ level_map = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40}
225
+ current_threshold = level_map.get(self.current_log_level, 20)
226
+
227
+ for idx, (text, level) in enumerate(self._log_store):
228
+ # Always show system messages (level >= 100) or if meets threshold
229
+ if level >= 100 or level >= current_threshold:
230
+ self._add_log_to_table(text, idx)
231
+
232
+ except Exception:
233
+ pass
234
+
235
+ def _update_row_appearance(self, store_index: int):
236
+ """Updates the appearance of a single row based on selection state."""
237
+ try:
238
+ table = self.query_one("#log-window", DataTable)
239
+ if 0 <= store_index < len(self._log_store):
240
+ text, _ = self._log_store[store_index]
241
+ if store_index in self._selected_indices:
242
+ # User requested NO caret, just highlight
243
+ display_text = text
244
+ display_text.stylize("bold white on $secondary")
245
+ else:
246
+ display_text = text
247
+
248
+ # We need the key as string
249
+ table.update_cell(str(store_index), "log", display_text)
250
+ except Exception:
251
+ pass
252
+
253
+ def handle_log_mouse_down(self, store_index_str: str, shift: bool = False) -> bool:
254
+ """Handle mouse down: toggle single or start range."""
255
+ try:
256
+ store_index = int(store_index_str)
257
+ except ValueError:
258
+ return False
259
+
260
+ if shift and self._last_selected_index is not None:
261
+ # Shift+Click (immediate range)
262
+ # Clear purely if we want standard behavior?
263
+ # Or add to selection?
264
+ # Standard: Select range from anchor to here.
265
+ # We clear current explicit selection if it's a new range?
266
+ # User said: "shift click to select a range".
267
+ # Usually implies resetting selection to just that range?
268
+ # Or extending?
269
+ # Let's say: Reset others, select range.
270
+ self.action_deselect_all_silent()
271
+
272
+ start = min(self._last_selected_index, store_index)
273
+ end = max(self._last_selected_index, store_index)
274
+ for i in range(start, end + 1):
275
+ self._selected_indices.add(i)
276
+
277
+ # Don't update last_selected_index on shift-click usually, or do?
278
+ # Usually Shift+Click preserves anchor.
279
+ # We keep _last_selected_index as anchor.
280
+ else:
281
+ # Regular Click/Drag Start
282
+ # If simple click: Toggle? Or select exclusive?
283
+ # User liked "clicking added/removed" (Toggle).
284
+ # We will toggle.
285
+ if store_index in self._selected_indices:
286
+ self._selected_indices.remove(store_index)
287
+ else:
288
+ self._selected_indices.add(store_index)
289
+
290
+ self._last_selected_index = store_index
291
+ self._drag_start_index = store_index # Anchor for drag
292
+
293
+ self.refresh_visible_rows()
294
+ return True # Capture mouse
295
+
296
+ def handle_log_mouse_move(self, store_index_str: str):
297
+ """Handle drag: extend selection from drag anchor."""
298
+ try:
299
+ current_index = int(store_index_str)
300
+ except ValueError:
301
+ return
302
+
303
+ if self._drag_start_index is not None:
304
+ # Select range [start, current]
305
+ # But wait, we want to toggle them? Or force select?
306
+ # Drag usually force selects.
307
+ # We should probably clear OTHER selections if we assume standard behavior?
308
+ # But user likes toggle.
309
+ # "Click + Drag" usually means: Select everything in dragged range.
310
+ # We'll validly set everything in range to selected.
311
+
312
+ start = min(self._drag_start_index, current_index)
313
+ end = max(self._drag_start_index, current_index)
314
+
315
+ # Optimization: only update what changed?
316
+ # For now, just add loop.
317
+ for i in range(start, end + 1):
318
+ self._selected_indices.add(i)
319
+
320
+ self._last_selected_index = current_index
321
+ self.refresh_visible_rows()
322
+
323
+ def action_deselect_all_silent(self):
324
+ """Deselect without refresh (internal)."""
325
+ self._selected_indices.clear()
326
+
327
+ def refresh_visible_rows(self):
328
+ """Efficiently update appearance of rows."""
329
+ # This is expensive if we do ALL.
330
+ # But we only need to update visible or changed?
331
+ # For simplicity, we loop log store for now, or just trust reactive updates?
332
+ # We need to manually call _update_row_appearance.
333
+ # Instead of updating ALL (expensive), we should track what we acted on.
334
+ # But for 'deselect all' we need to update all old ones.
335
+
336
+ # We'll just force refresh of current view - or just update rows that CHANGED?
337
+ # That requires diffing.
338
+ # Let's iterate all valid indices in table?
339
+ # We can iterate self._log_store and update.
340
+ # We'll accept O(N) for now (~1000 lines is fine, 100k is slow).
341
+ # We can optimize later.
342
+
343
+ # Actually, iterating 0 to len(_log_store) and calling update_cell on every mouse move is BAD.
344
+ # We should optimize handle_log_mouse_move to only update specific rows.
345
+ # But for now, we leave it to be safe on logic.
346
+
347
+ # Better: _update_row_appearance only calls update_cell.
348
+ # We can just iterate visible range?
349
+ try:
350
+ # Just iterate all simple for correctness first.
351
+ for i in range(len(self._log_store)):
352
+ self._update_row_appearance(i)
353
+ except Exception:
354
+ pass
355
+
356
+ def action_toggle_log_level(self):
357
+ self.current_log_level_index = (self.current_log_level_index + 1) % len(
358
+ self.log_levels
359
+ )
360
+ self.update_uptime() # Update header
361
+ self.refresh_log_view()
362
+
363
+ def action_toggle_selection(self):
364
+ """Toggle selection of the current row."""
365
+ try:
366
+ table = self.query_one("#log-window", DataTable)
367
+ cursor_row = table.cursor_row
368
+ if cursor_row is None:
369
+ return
370
+
371
+ row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
372
+ if row_key.value is None:
373
+ return
374
+ store_index = int(row_key.value)
375
+
376
+ if store_index in self._selected_indices:
377
+ self._selected_indices.remove(store_index)
378
+ else:
379
+ self._selected_indices.add(store_index)
380
+
381
+ self._last_selected_index = store_index
382
+ self._update_row_appearance(store_index)
383
+
384
+ except Exception:
385
+ pass
386
+
387
+ def action_deselect_all(self):
388
+ """Deselect all rows."""
389
+ old_indices = list(self._selected_indices)
390
+ self._selected_indices.clear()
391
+ self._last_selected_index = None
392
+ for idx in old_indices:
393
+ self._update_row_appearance(idx)
394
+
395
+ async def run_server(self):
396
+ """Runs the actual Uvicorn server as a subprocess."""
397
+ # Force UTF-8 encoding for the subprocess to avoid UnicodeEncodeError on Windows
398
+ env = os.environ.copy()
399
+ env["PYTHONIOENCODING"] = "utf-8"
400
+
401
+ self.server_process = await asyncio.create_subprocess_exec(
402
+ *self.command,
403
+ stdout=asyncio.subprocess.PIPE,
404
+ stderr=asyncio.subprocess.PIPE,
405
+ env=env,
406
+ )
407
+
408
+ async def read_stream(stream):
409
+ while True:
410
+ line = await stream.readline()
411
+ if not line:
412
+ break
413
+ text_content = line.decode().strip()
414
+
415
+ if "Press CTRL+C to quit" in text_content:
416
+ text_content = text_content.replace(
417
+ "Press CTRL+C to quit", "Press q to quit"
418
+ )
419
+
420
+ # Determine level
421
+ level = 20 # Default INFO
422
+ upper_text = text_content.upper()
423
+ if "DEBUG" in upper_text:
424
+ level = 10
425
+ elif "INFO" in upper_text:
426
+ level = 20
427
+ elif "WARNING" in upper_text:
428
+ level = 30
429
+ elif "ERROR" in upper_text:
430
+ level = 40
431
+
432
+ # System/Always visible messages logic
433
+ if "PyWire" in text_content:
434
+ if "Error" in text_content:
435
+ level = 40
436
+ elif "Warning" in text_content:
437
+ level = 30
438
+ else:
439
+ level = 20
440
+
441
+ renderable = Text.from_ansi(text_content)
442
+ self.log_write(renderable, level=level)
443
+
444
+ if self.server_process.stdout and self.server_process.stderr:
445
+ try:
446
+ await asyncio.gather(
447
+ read_stream(self.server_process.stdout),
448
+ read_stream(self.server_process.stderr),
449
+ )
450
+ except asyncio.CancelledError:
451
+ # Task cancelled (shutdown)
452
+ pass
453
+
454
+ async def action_restart_server(self):
455
+ """Kill and restart the subprocess."""
456
+ self.log_write("\n[bold magenta]↻ Restarting Server...[/]\n", level=100)
457
+
458
+ if self.server_process:
459
+ try:
460
+ self.server_process.terminate()
461
+ await self.server_process.wait()
462
+ except ProcessLookupError:
463
+ pass
464
+
465
+ self.start_time = time.time()
466
+ # Cancel old task?
467
+ if hasattr(self, "server_task"):
468
+ self.server_task.cancel()
469
+ self.server_task = asyncio.create_task(self.run_server())
470
+
471
+ def action_clear_logs(self):
472
+ try:
473
+ self.query_one("#log-window", DataTable).clear()
474
+ except Exception:
475
+ pass
476
+ self._log_store = []
477
+ self._selected_indices = set()
478
+ self._last_selected_index = None
479
+
480
+ def action_copy_logs(self):
481
+ """Copy current log view to system clipboard."""
482
+ lines = []
483
+
484
+ # If selection exists, copy only selection
485
+ if self._selected_indices:
486
+ sorted_indices = sorted(self._selected_indices)
487
+ for idx in sorted_indices:
488
+ if 0 <= idx < len(self._log_store):
489
+ text_obj, _ = self._log_store[idx]
490
+ plain = text_obj.plain
491
+ # Exclude tips if needed? User said "Tip: ... to be part of output" (Don't want)
492
+ if "Tip: Use Space or Click" in plain:
493
+ continue
494
+ lines.append(plain)
495
+ else:
496
+ # Copy all visible logs
497
+ level_map = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40}
498
+ current_threshold = level_map.get(self.current_log_level, 20)
499
+ for text_obj, level in self._log_store:
500
+ # Level 100 are system messages. User said "don't want Copied n lines..."
501
+ # Copied messages are level 100.
502
+ # We should probably filters out "Copied ..." messages themselves?
503
+ # Or just not store Copied messages in the log store?
504
+ # Ah, log_write stores them.
505
+ plain = text_obj.plain
506
+ if "Copied" in plain and "lines" in plain and "clipboard" in plain:
507
+ continue
508
+ if "Tip: Use Space or Click" in plain:
509
+ continue
510
+
511
+ if level >= 100 or level >= current_threshold:
512
+ lines.append(plain)
513
+
514
+ content = "\n".join(lines)
515
+
516
+ copied = False
517
+ try:
518
+ if sys.platform == "darwin" and shutil.which("pbcopy"):
519
+ subprocess.run("pbcopy", input=content, text=True)
520
+ copied = True
521
+ elif sys.platform.startswith("linux"):
522
+ if shutil.which("wl-copy"):
523
+ subprocess.run("wl-copy", input=content, text=True)
524
+ copied = True
525
+ elif shutil.which("xclip"):
526
+ subprocess.run(
527
+ ["xclip", "-selection", "clipboard"], input=content, text=True
528
+ )
529
+ copied = True
530
+ elif sys.platform == "win32":
531
+ subprocess.run("clip", input=content, text=True)
532
+ copied = True
533
+ except Exception as e:
534
+ self.notify(f"Copy failed: {e}", severity="error")
535
+ return
536
+
537
+ if copied:
538
+ self.notify(f"Copied {len(lines)} lines to clipboard!")
539
+ # Deselect lines after copying
540
+ self.action_deselect_all()
541
+ else:
542
+ self.notify("Clipboard tool not found.", severity="error")
543
+
544
+
545
+ def start_tui(
546
+ app_path: str,
547
+ host: str,
548
+ port: int,
549
+ ssl_keyfile: str | None,
550
+ ssl_certfile: str | None,
551
+ env_file: str | None,
552
+ ) -> None:
553
+ cmd = [
554
+ sys.executable,
555
+ "-m",
556
+ "pywire_cli.main",
557
+ "dev",
558
+ app_path,
559
+ "--no-tui",
560
+ "--host",
561
+ host,
562
+ "--port",
563
+ str(port),
564
+ ]
565
+
566
+ if ssl_keyfile:
567
+ cmd.extend(["--ssl-keyfile", ssl_keyfile])
568
+ if ssl_certfile:
569
+ cmd.extend(["--ssl-certfile", ssl_certfile])
570
+ if env_file:
571
+ cmd.extend(["--env-file", env_file])
572
+
573
+ app = PyWireDevDashboard(cmd, host, port)
574
+ app.run()
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywire-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line tools for PyWire projects: dev, build, deploy, check
5
+ Project-URL: Homepage, https://pywire.dev
6
+ Project-URL: Documentation, https://pywire.dev/docs
7
+ Project-URL: Repository, https://github.com/pywire/pywire
8
+ Project-URL: Issues, https://github.com/pywire/pywire/issues
9
+ Author-email: Reece Holmdahl <reece@pywire.dev>
10
+ License-Expression: MIT
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: jinja2>=3.1.0
21
+ Requires-Dist: pywire-templates
22
+ Requires-Dist: pywire[build]
23
+ Requires-Dist: rich-click>=1.9.6
24
+ Requires-Dist: textual>=7.4.0
25
+ Requires-Dist: uvicorn[standard]>=0.27.0
26
+ Requires-Dist: watchfiles>=0.21.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
32
+ Requires-Dist: ty; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # pywire-cli
36
+
37
+ Command-line tools for [PyWire](https://pywire.dev) projects: `dev`, `run`, `build`, `deploy`, `check`.
38
+
39
+ Normally installed via the `pywire[cli]` extra:
40
+
41
+ ```sh
42
+ uv add pywire[cli]
43
+ # or
44
+ pip install pywire[cli]
45
+ ```
46
+
47
+ This installs `pywire-cli` alongside the core framework and makes the `pywire` command available.
48
+
49
+ ## Commands
50
+
51
+ - `pywire dev` — run the development server with hot reload
52
+ - `pywire run` — run the production server
53
+ - `pywire build` — compile the project for production
54
+ - `pywire deploy` — generate deployment configs (Docker, Render, Fly, Railway, Cloudflare)
55
+ - `pywire check` — run static analysis on the project (non-serializable wires, reactivity errors, redundant patterns)
56
+ - `pywire config` — read/write PyWire settings
@@ -0,0 +1,10 @@
1
+ pywire_cli/__init__.py,sha256=pSXwobn3tzZgJqoYdnURQsIuLTKpLqtUCrYa37OWtRM,101
2
+ pywire_cli/check.py,sha256=oyRR6i5JjNBcMJaWbOuAmxOSbfQYiYUvX1UwNAnVc9U,3660
3
+ pywire_cli/config.py,sha256=7Sx_CnWTt5kst8GZd2i434DvBwZ5SEF1tEr1dkD-cOs,4501
4
+ pywire_cli/deploy.py,sha256=O5nmea4jCVwFhnM6wP4Mcwq1eMTTT2yApJ5iNdFdYiU,3154
5
+ pywire_cli/main.py,sha256=gUP6V9GRkz1xzJNqnxjggdAQsn2eDPJBTiYO4W9hIE8,30964
6
+ pywire_cli/tui.py,sha256=ZdF63wI7sZEWfSrCJUUmMZ2SkswLyTWaKcjlcv2x7DI,22142
7
+ pywire_cli-0.2.0.dist-info/METADATA,sha256=lJ7vqzHAe8HX_iJmVo43YneK-3YgBRnZshcGtwYWCkA,2097
8
+ pywire_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ pywire_cli-0.2.0.dist-info/entry_points.txt,sha256=IVxsFE0EM8RBv2MVqxK4uZySNppT80snJy7kNR12xio,47
10
+ pywire_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pywire = pywire_cli.main:cli