dars-framework 1.2.1__tar.gz → 1.2.3__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.
- {dars_framework-1.2.1/dars_framework.egg-info → dars_framework-1.2.3}/PKG-INFO +3 -3
- {dars_framework-1.2.1 → dars_framework-1.2.3}/README.md +4 -1
- dars_framework-1.2.3/dars/cli/doctor/__init__.py +1 -0
- dars_framework-1.2.3/dars/cli/doctor/detect.py +154 -0
- dars_framework-1.2.3/dars/cli/doctor/doctor.py +176 -0
- dars_framework-1.2.3/dars/cli/doctor/installers.py +100 -0
- dars_framework-1.2.3/dars/cli/doctor/persist.py +62 -0
- dars_framework-1.2.3/dars/cli/doctor/preflight.py +33 -0
- dars_framework-1.2.3/dars/cli/doctor/ui.py +54 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/main.py +74 -15
- dars_framework-1.2.3/dars/core/js_bridge.py +99 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/properties.py +3 -3
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/state.py +54 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/html_css_js.py +24 -1
- dars_framework-1.2.3/dars/js_lib.py +206 -0
- dars_framework-1.2.3/dars/security.py +195 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/dState/state_mods_demo.py +2 -5
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/version.py +2 -2
- {dars_framework-1.2.1 → dars_framework-1.2.3/dars_framework.egg-info}/PKG-INFO +3 -3
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/SOURCES.txt +8 -9
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/requires.txt +1 -1
- {dars_framework-1.2.1 → dars_framework-1.2.3}/pyproject.toml +3 -5
- dars_framework-1.2.1/dars/js_lib.py +0 -133
- dars_framework-1.2.1/dars/security.py +0 -133
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/index.html +0 -72
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/lib/dars.min.js +0 -131
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/runtime_css.css +0 -676
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/runtime_dars.js +0 -349
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/script.js +0 -1
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/snapshot.json +0 -1
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/styles.css +0 -0
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/vdom_tree.js +0 -1
- dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/version.txt +0 -1
- {dars_framework-1.2.1 → dars_framework-1.2.3}/LICENSE +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/MANIFEST.in +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/all.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/hot_reload.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/preview.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/translations.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/accordion.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/card.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/modal.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/navbar.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/table.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/tabs.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/button.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/checkbox.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/container.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/datepicker.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/image.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/input.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/link.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/markdown.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/page.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/progressbar.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/radiobutton.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/select.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/slider.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/spinner.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/text.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/textarea.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/tooltip.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/anchor.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/flex.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/grid.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/config.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/app.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/component.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/events.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/apps_test/health_check.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/run_tests.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_advanced_components.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_basic_components.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_core_and_cli.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_layout_components.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_version_check.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/app.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/cli.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/components.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/custom_components.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/events.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/exporters.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/getting_started.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/index.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/scripts.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/state_management.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/base.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_OLD4.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_old.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_old2.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/vdom.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/dscript.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/script.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/README.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/SimpleDashboard/dashboard.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/VariousComponents/all_components_demo.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Forms/form_components.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Forms/simple_form.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/HelloWorld/hello_world.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/flex_layout_responsive.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/grid_layout_responsive.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/layout_multipage_demo.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Multipage/multipage_example.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/pwa_custom_icons.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/demo/complete_app.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/README.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/html/__init__.py +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/dependency_links.txt +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/entry_points.txt +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/top_level.txt +0 -0
- {dars_framework-1.2.1 → dars_framework-1.2.3}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dars_framework
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Dars is a Python UI framework for building modern, interactive web apps with only Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
|
|
5
5
|
Author-email: ztamdev <zondax2009@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
|
-
Requires-Dist: rich==14.
|
|
8
|
+
Requires-Dist: rich==14.2.0
|
|
9
9
|
Requires-Dist: bs4==0.0.2
|
|
10
10
|
Requires-Dist: uvicorn==0.35.0
|
|
11
11
|
Requires-Dist: fastapi==0.116.1
|
|
@@ -21,7 +21,7 @@ Try dars without installing nothing just visit the [Dars Playground](https://dar
|
|
|
21
21
|
- Preview instantly with hot-reload using `app.rTimeCompile()`.
|
|
22
22
|
- Export your app to static web files with a single CLI command.
|
|
23
23
|
- Use multipage, layouts, scripts, and more—see docs for advanced features.
|
|
24
|
-
- For
|
|
24
|
+
- For more information visit the [Documentation](https://ztamdev.github.io/Dars-Framework/documentation.html)
|
|
25
25
|
|
|
26
26
|
## Quick Example: Your First App
|
|
27
27
|
```python
|
|
@@ -218,3 +218,6 @@ Export using the config entry and outdir:
|
|
|
218
218
|
dars export config --format html
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
See LandingPage docs for details: state_management.md, events.md, scripts.md.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# dars.cli.doctor package
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import os, subprocess, sys, re
|
|
2
|
+
from typing import Tuple, Optional, List, Dict
|
|
3
|
+
from dars.core.js_bridge import has_bun, has_node, _run as js_run
|
|
4
|
+
|
|
5
|
+
SEMVER_RE = re.compile(r"v?(\d+)\.(\d+)\.(\d+)")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _run(cmd: List[str]) -> Tuple[int, str, str]:
|
|
9
|
+
try:
|
|
10
|
+
p = subprocess.run(cmd, capture_output=True, text=True, shell=False)
|
|
11
|
+
return p.returncode, p.stdout.strip(), p.stderr.strip()
|
|
12
|
+
except Exception as e:
|
|
13
|
+
return 1, "", str(e)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def which(bin_name: str) -> Optional[str]:
|
|
17
|
+
if os.name == 'nt':
|
|
18
|
+
code, out, _ = _run(["where", bin_name])
|
|
19
|
+
else:
|
|
20
|
+
code, out, _ = _run(["which", bin_name])
|
|
21
|
+
if code == 0 and out:
|
|
22
|
+
return out.splitlines()[0].strip()
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_semver(s: str) -> Optional[str]:
|
|
27
|
+
m = SEMVER_RE.search(s or "")
|
|
28
|
+
if not m:
|
|
29
|
+
return None
|
|
30
|
+
return f"{m.group(1)}.{m.group(2)}.{m.group(3)}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_node() -> Dict[str, Optional[str]]:
|
|
34
|
+
path = which("node")
|
|
35
|
+
if not path:
|
|
36
|
+
return {"ok": False, "version": None, "path": None}
|
|
37
|
+
code, out, _ = _run([path, "--version"]) # prints like v18.19.1
|
|
38
|
+
ver = parse_semver(out)
|
|
39
|
+
return {"ok": bool(ver), "version": ver, "path": path}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_esbuild() -> Dict[str, Optional[str]]:
|
|
43
|
+
# Prefer bun x esbuild --version
|
|
44
|
+
if has_bun():
|
|
45
|
+
code, out, err = js_run(["bun", "x", "esbuild", "--version"]) # type: ignore
|
|
46
|
+
if code == 0:
|
|
47
|
+
return {"ok": True, "version": out.strip() or "unknown", "path": "bun x esbuild"}
|
|
48
|
+
# Node fallback
|
|
49
|
+
if has_node():
|
|
50
|
+
code, out, err = js_run(["npx", "--yes", "esbuild", "--version"]) # type: ignore
|
|
51
|
+
if code == 0:
|
|
52
|
+
return {"ok": True, "version": out.strip() or "unknown", "path": "npx esbuild"}
|
|
53
|
+
return {"ok": False, "version": None, "path": None}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_vite() -> Dict[str, Optional[str]]:
|
|
57
|
+
# Prefer bun x vite --version
|
|
58
|
+
if has_bun():
|
|
59
|
+
code, out, err = js_run(["bun", "x", "vite", "--version"]) # type: ignore
|
|
60
|
+
if code == 0:
|
|
61
|
+
return {"ok": True, "version": out.strip() or "unknown", "path": "bun x vite"}
|
|
62
|
+
if has_node():
|
|
63
|
+
code, out, err = js_run(["npx", "--yes", "vite", "--version"]) # type: ignore
|
|
64
|
+
if code == 0:
|
|
65
|
+
return {"ok": True, "version": out.strip() or "unknown", "path": "npx vite"}
|
|
66
|
+
return {"ok": False, "version": None, "path": None}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def detect_bun() -> Dict[str, Optional[str]]:
|
|
70
|
+
path = which("bun")
|
|
71
|
+
if not path:
|
|
72
|
+
return {"ok": False, "version": None, "path": None}
|
|
73
|
+
code, out, _ = _run([path, "--version"]) # prints like 1.1.24
|
|
74
|
+
ver = parse_semver(out) or out.strip()
|
|
75
|
+
return {"ok": bool(ver), "version": ver, "path": path}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def read_pyproject_deps(pyproject_path: Optional[str] = None) -> List[str]:
|
|
79
|
+
"""Extract [project].dependencies items as a list of requirement strings.
|
|
80
|
+
Avoids pulling unrelated keys (e.g., license).
|
|
81
|
+
"""
|
|
82
|
+
path = pyproject_path or os.path.join(os.getcwd(), "pyproject.toml")
|
|
83
|
+
if not os.path.isfile(path):
|
|
84
|
+
return []
|
|
85
|
+
try:
|
|
86
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
87
|
+
lines = f.readlines()
|
|
88
|
+
reqs: List[str] = []
|
|
89
|
+
in_project = False
|
|
90
|
+
in_array = False
|
|
91
|
+
buf: List[str] = []
|
|
92
|
+
for raw in lines:
|
|
93
|
+
l = raw.strip()
|
|
94
|
+
# Track project table
|
|
95
|
+
if l.startswith('['):
|
|
96
|
+
in_project = (l == '[project]')
|
|
97
|
+
# leaving dependencies array if we hit a new table
|
|
98
|
+
if l != '[project]':
|
|
99
|
+
in_array = False
|
|
100
|
+
continue
|
|
101
|
+
if not in_project:
|
|
102
|
+
continue
|
|
103
|
+
# Find dependencies array start
|
|
104
|
+
if not in_array and l.startswith('dependencies') and '=' in l:
|
|
105
|
+
# Could be inline or multiline
|
|
106
|
+
# Normalize to everything after '='
|
|
107
|
+
after = l.split('=', 1)[1].strip()
|
|
108
|
+
if after.startswith('[') and after.endswith(']'):
|
|
109
|
+
# Inline array
|
|
110
|
+
buf = [after]
|
|
111
|
+
in_array = False
|
|
112
|
+
elif after.startswith('['):
|
|
113
|
+
buf = [after]
|
|
114
|
+
in_array = True
|
|
115
|
+
else:
|
|
116
|
+
# Malformed; skip
|
|
117
|
+
buf = []
|
|
118
|
+
in_array = False
|
|
119
|
+
# If inline, fall-through to extraction below
|
|
120
|
+
elif in_array:
|
|
121
|
+
buf.append(l)
|
|
122
|
+
if ']' in l:
|
|
123
|
+
in_array = False
|
|
124
|
+
# When we have a complete buffer (inline or closed multiline), extract
|
|
125
|
+
if buf and not in_array:
|
|
126
|
+
content = ' '.join(buf)
|
|
127
|
+
# Extract quoted items within brackets
|
|
128
|
+
m = re.findall(r'"([^"\\]*(?:\\.[^"\\]*)*)"', content)
|
|
129
|
+
for item in m:
|
|
130
|
+
if item:
|
|
131
|
+
reqs.append(item)
|
|
132
|
+
buf = []
|
|
133
|
+
return reqs
|
|
134
|
+
except Exception:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check_python_deps(requirements: List[str]) -> Dict[str, List[str]]:
|
|
139
|
+
missing: List[str] = []
|
|
140
|
+
try:
|
|
141
|
+
try:
|
|
142
|
+
import importlib.metadata as md # py3.8+
|
|
143
|
+
except Exception:
|
|
144
|
+
import importlib_metadata as md # type: ignore
|
|
145
|
+
for spec in requirements:
|
|
146
|
+
name = spec.split("==")[0].split(">=")[0].split("<=")[0]
|
|
147
|
+
try:
|
|
148
|
+
md.version(name)
|
|
149
|
+
except Exception:
|
|
150
|
+
missing.append(spec)
|
|
151
|
+
except Exception:
|
|
152
|
+
# if detection fails, don't block
|
|
153
|
+
pass
|
|
154
|
+
return {"missing": missing}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
from .detect import detect_node, detect_bun, detect_esbuild, detect_vite, read_pyproject_deps, check_python_deps
|
|
4
|
+
from .installers import install_node, install_bun, install_esbuild, install_vite
|
|
5
|
+
from .persist import load_config, save_config
|
|
6
|
+
from .ui import render_report, prompt_action, confirm_install
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_doctor(check_only: bool = False, auto_yes: bool = False, install_all: bool = False, force: bool = False) -> int:
|
|
13
|
+
cfg = load_config()
|
|
14
|
+
|
|
15
|
+
# Detect with spinner
|
|
16
|
+
with console.status("[cyan]Checking environment...[/cyan]"):
|
|
17
|
+
node = detect_node()
|
|
18
|
+
bun = detect_bun()
|
|
19
|
+
esb = detect_esbuild()
|
|
20
|
+
vit = detect_vite()
|
|
21
|
+
reqs = read_pyproject_deps()
|
|
22
|
+
py = check_python_deps(reqs)
|
|
23
|
+
|
|
24
|
+
render_report(node, bun, py, esb, vit)
|
|
25
|
+
|
|
26
|
+
missing_items: List[str] = []
|
|
27
|
+
if not node.get('ok'): missing_items.append('Node.js LTS')
|
|
28
|
+
if not bun.get('ok'): missing_items.append('Bun stable')
|
|
29
|
+
if py.get('missing'): missing_items.append('Python deps')
|
|
30
|
+
optional_missing: List[str] = []
|
|
31
|
+
if not esb.get('ok'): optional_missing.append('esbuild (optional)')
|
|
32
|
+
if not vit.get('ok'): optional_missing.append('vite (optional)')
|
|
33
|
+
|
|
34
|
+
if check_only:
|
|
35
|
+
return 0 if not missing_items else 1
|
|
36
|
+
|
|
37
|
+
# Decide next steps
|
|
38
|
+
has_missing = bool(missing_items)
|
|
39
|
+
|
|
40
|
+
# Always show a small action menu; if nothing missing, offer re-run/quit
|
|
41
|
+
if not check_only:
|
|
42
|
+
choice = '1' if (auto_yes and install_all and has_missing) else prompt_action(has_missing)
|
|
43
|
+
if has_missing:
|
|
44
|
+
if choice == '3':
|
|
45
|
+
return 1
|
|
46
|
+
if choice == '2':
|
|
47
|
+
return run_doctor(check_only=False, auto_yes=auto_yes, install_all=install_all, force=True)
|
|
48
|
+
else:
|
|
49
|
+
# No missing: '1' => re-run, '2' => quit
|
|
50
|
+
if choice == '2':
|
|
51
|
+
return 0
|
|
52
|
+
if choice == '1':
|
|
53
|
+
return run_doctor(check_only=False, auto_yes=auto_yes, install_all=install_all, force=True)
|
|
54
|
+
|
|
55
|
+
if not has_missing:
|
|
56
|
+
cfg['requirements']['node'].update({'ok': True, 'version': node.get('version')})
|
|
57
|
+
cfg['requirements']['bun'].update({'ok': True, 'version': bun.get('version')})
|
|
58
|
+
cfg['python_deps'] = {'ok': True, 'missing': []}
|
|
59
|
+
cfg['satisfied'] = True
|
|
60
|
+
save_config(cfg)
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
if auto_yes and install_all:
|
|
64
|
+
choice = '1'
|
|
65
|
+
else:
|
|
66
|
+
choice = prompt_action()
|
|
67
|
+
if choice == '3':
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
if choice == '2':
|
|
71
|
+
# Re-run immediately
|
|
72
|
+
return run_doctor(check_only=False, auto_yes=auto_yes, install_all=install_all, force=True)
|
|
73
|
+
|
|
74
|
+
# choice == '1' => Install ALL missing
|
|
75
|
+
summary: List[str] = []
|
|
76
|
+
if not node.get('ok'): summary.append('Node.js LTS (winget)')
|
|
77
|
+
if not bun.get('ok'): summary.append('Bun (winget)')
|
|
78
|
+
if py.get('missing'): summary.append(f"Python deps: {', '.join(py['missing'])}")
|
|
79
|
+
if optional_missing:
|
|
80
|
+
summary.extend(optional_missing)
|
|
81
|
+
|
|
82
|
+
if not auto_yes:
|
|
83
|
+
if not confirm_install(summary):
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
# Installers: always run Node then Bun sequentially (idempotent if already installed)
|
|
87
|
+
with console.status("[cyan]Installing selected items...[/cyan]"):
|
|
88
|
+
try:
|
|
89
|
+
install_node()
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
try:
|
|
93
|
+
install_bun()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
# Optional developer tools
|
|
97
|
+
try:
|
|
98
|
+
if not esb.get('ok'):
|
|
99
|
+
install_esbuild()
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
if not vit.get('ok'):
|
|
104
|
+
install_vite()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Python deps via pip
|
|
109
|
+
if py.get('missing'):
|
|
110
|
+
try:
|
|
111
|
+
import subprocess
|
|
112
|
+
cmd = [sys.executable, '-m', 'pip', 'install', '--upgrade'] + py['missing']
|
|
113
|
+
subprocess.run(cmd, check=False)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Re-check after attempted install
|
|
118
|
+
with console.status("[cyan]Re-checking...[/cyan]"):
|
|
119
|
+
node2 = detect_node()
|
|
120
|
+
bun2 = detect_bun()
|
|
121
|
+
esb2 = detect_esbuild()
|
|
122
|
+
vit2 = detect_vite()
|
|
123
|
+
py2 = check_python_deps(read_pyproject_deps())
|
|
124
|
+
|
|
125
|
+
render_report(node2, bun2, py2, esb2, vit2)
|
|
126
|
+
|
|
127
|
+
all_ok = node2.get('ok') and bun2.get('ok') and not py2.get('missing')
|
|
128
|
+
|
|
129
|
+
cfg['requirements']['node'].update({'ok': bool(node2.get('ok')), 'version': node2.get('version')})
|
|
130
|
+
cfg['requirements']['bun'].update({'ok': bool(bun2.get('ok')), 'version': bun2.get('version')})
|
|
131
|
+
cfg['python_deps'] = {'ok': not bool(py2.get('missing')), 'missing': py2.get('missing') or []}
|
|
132
|
+
cfg['satisfied'] = bool(all_ok)
|
|
133
|
+
save_config(cfg)
|
|
134
|
+
|
|
135
|
+
return 0 if all_ok else 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_forcedev() -> int:
|
|
139
|
+
"""Force-install everything without initial verification or prompts.
|
|
140
|
+
- Attempts Node LTS and Bun installers unconditionally (best-effort)
|
|
141
|
+
- Installs/updates all Python deps from pyproject.toml
|
|
142
|
+
- Re-checks and persists satisfied state
|
|
143
|
+
Returns 0 if environment ends OK, else 1.
|
|
144
|
+
"""
|
|
145
|
+
# Best-effort installs (no UI)
|
|
146
|
+
try:
|
|
147
|
+
install_node()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
try:
|
|
151
|
+
install_bun()
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
reqs = read_pyproject_deps()
|
|
156
|
+
if reqs:
|
|
157
|
+
try:
|
|
158
|
+
import subprocess, sys as _sys
|
|
159
|
+
cmd = [_sys.executable, '-m', 'pip', 'install', '--upgrade'] + reqs
|
|
160
|
+
subprocess.run(cmd, check=False)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Re-check and persist
|
|
165
|
+
cfg = load_config()
|
|
166
|
+
node2 = detect_node()
|
|
167
|
+
bun2 = detect_bun()
|
|
168
|
+
py2 = check_python_deps(read_pyproject_deps())
|
|
169
|
+
all_ok = node2.get('ok') and bun2.get('ok') and not py2.get('missing')
|
|
170
|
+
|
|
171
|
+
cfg['requirements']['node'].update({'ok': bool(node2.get('ok')), 'version': node2.get('version')})
|
|
172
|
+
cfg['requirements']['bun'].update({'ok': bool(bun2.get('ok')), 'version': bun2.get('version')})
|
|
173
|
+
cfg['python_deps'] = {'ok': not bool(py2.get('missing')), 'missing': py2.get('missing') or []}
|
|
174
|
+
cfg['satisfied'] = bool(all_ok)
|
|
175
|
+
save_config(cfg)
|
|
176
|
+
return 0 if all_ok else 1
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os, sys, subprocess
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from dars.core.js_bridge import has_bun
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(cmd: list) -> Tuple[int, str, str]:
|
|
7
|
+
try:
|
|
8
|
+
p = subprocess.run(cmd, capture_output=True, text=True, shell=False)
|
|
9
|
+
return p.returncode, p.stdout.strip(), p.stderr.strip()
|
|
10
|
+
except Exception as e:
|
|
11
|
+
return 1, "", str(e)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_live(cmd: list, shell: bool = False) -> int:
|
|
15
|
+
"""Run a command inheriting stdout/stderr so the user sees prompts/output live."""
|
|
16
|
+
try:
|
|
17
|
+
p = subprocess.run(cmd, shell=shell)
|
|
18
|
+
return p.returncode or 0
|
|
19
|
+
except Exception:
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- Windows installers (preferred: winget) ---
|
|
24
|
+
|
|
25
|
+
def install_node_windows() -> Tuple[bool, str]:
|
|
26
|
+
# OpenJS.NodeJS.LTS is the winget package id for Node.js LTS
|
|
27
|
+
code, out, err = run(["winget", "install", "-e", "--id", "OpenJS.NodeJS.LTS"])
|
|
28
|
+
ok = (code == 0)
|
|
29
|
+
return ok, (out or err)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def install_bun_windows() -> Tuple[bool, str]:
|
|
33
|
+
# 1) Try winget first (Oven-sh.Bun)
|
|
34
|
+
code, out, err = run(["winget", "install", "-e", "--id", "Oven-sh.Bun"])
|
|
35
|
+
if code == 0:
|
|
36
|
+
return True, (out or err)
|
|
37
|
+
# 2) Fallback: official bun.sh PowerShell installer (streams output)
|
|
38
|
+
# Command shown to the user for transparency
|
|
39
|
+
print("Executing: powershell -NoProfile -ExecutionPolicy Bypass -c \"irm bun.sh/install.ps1 | iex\"")
|
|
40
|
+
ps_cmd = [
|
|
41
|
+
"powershell",
|
|
42
|
+
"-NoProfile",
|
|
43
|
+
"-ExecutionPolicy",
|
|
44
|
+
"Bypass",
|
|
45
|
+
"-c",
|
|
46
|
+
"irm bun.sh/install.ps1 | iex",
|
|
47
|
+
]
|
|
48
|
+
code2 = run_live(ps_cmd, shell=False)
|
|
49
|
+
return code2 == 0, (out or err)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- Stubs for macOS/Linux (future extension) ---
|
|
53
|
+
|
|
54
|
+
def install_node_posix() -> Tuple[bool, str]:
|
|
55
|
+
# For now, provide guidance only
|
|
56
|
+
msg = "Please install Node.js LTS from https://nodejs.org (or use a package manager)."
|
|
57
|
+
return False, msg
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def install_bun_posix() -> Tuple[bool, str]:
|
|
61
|
+
# Use official installer with live output
|
|
62
|
+
# Prefer bash -lc; fallback to sh -lc
|
|
63
|
+
cmd_str = "curl -fsSL https://bun.sh/install | bash"
|
|
64
|
+
print(f"Executing: bash -lc \"{cmd_str}\"")
|
|
65
|
+
# Try bash first
|
|
66
|
+
code = run_live(["bash", "-lc", cmd_str], shell=False)
|
|
67
|
+
if code != 0:
|
|
68
|
+
print(f"bash failed, falling back to: sh -lc \"{cmd_str}\"")
|
|
69
|
+
code = run_live(["sh", "-lc", cmd_str], shell=False)
|
|
70
|
+
return code == 0, ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def install_node() -> Tuple[bool, str]:
|
|
74
|
+
if os.name == 'nt':
|
|
75
|
+
return install_node_windows()
|
|
76
|
+
return install_node_posix()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def install_bun() -> Tuple[bool, str]:
|
|
80
|
+
if os.name == 'nt':
|
|
81
|
+
return install_bun_windows()
|
|
82
|
+
return install_bun_posix()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def install_esbuild() -> Tuple[bool, str]:
|
|
86
|
+
# Prefer Bun-managed dev dep
|
|
87
|
+
if has_bun():
|
|
88
|
+
print("Executing: bun add -d esbuild")
|
|
89
|
+
code = run_live(["bun", "add", "-d", "esbuild"], shell=False)
|
|
90
|
+
return (code == 0, "")
|
|
91
|
+
# Node fallback: use npx without install (sufficient for detection), nothing to install
|
|
92
|
+
return True, ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def install_vite() -> Tuple[bool, str]:
|
|
96
|
+
if has_bun():
|
|
97
|
+
print("Executing: bun add -d vite")
|
|
98
|
+
code = run_live(["bun", "add", "-d", "vite"], shell=False)
|
|
99
|
+
return (code == 0, "")
|
|
100
|
+
return True, ""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json, os, sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, Tuple
|
|
4
|
+
|
|
5
|
+
APP_NAME = "Dars"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _config_base_dir() -> str:
|
|
9
|
+
# Windows
|
|
10
|
+
if os.name == 'nt':
|
|
11
|
+
base = os.getenv('APPDATA') or os.path.expanduser('~')
|
|
12
|
+
return os.path.join(base, APP_NAME)
|
|
13
|
+
# POSIX
|
|
14
|
+
xdg = os.getenv('XDG_CONFIG_HOME')
|
|
15
|
+
if xdg:
|
|
16
|
+
return os.path.join(xdg, 'dars')
|
|
17
|
+
return os.path.join(os.path.expanduser('~/.config'), 'dars')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_config_path() -> str:
|
|
21
|
+
d = _config_base_dir()
|
|
22
|
+
os.makedirs(d, exist_ok=True)
|
|
23
|
+
return os.path.join(d, 'config.json')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config() -> Dict[str, Any]:
|
|
27
|
+
p = get_config_path()
|
|
28
|
+
if not os.path.isfile(p):
|
|
29
|
+
return {
|
|
30
|
+
'requirements': {
|
|
31
|
+
'node': {'ok': False, 'version': None, 'source': None, 'checked_at': None},
|
|
32
|
+
'bun': {'ok': False, 'version': None, 'source': None, 'checked_at': None},
|
|
33
|
+
'optional': {}
|
|
34
|
+
},
|
|
35
|
+
'python_deps': {'ok': True, 'missing': [], 'checked_at': None},
|
|
36
|
+
'satisfied': False,
|
|
37
|
+
'last_doctor': None,
|
|
38
|
+
}
|
|
39
|
+
try:
|
|
40
|
+
with open(p, 'r', encoding='utf-8') as f:
|
|
41
|
+
return json.load(f)
|
|
42
|
+
except Exception:
|
|
43
|
+
return {
|
|
44
|
+
'requirements': {
|
|
45
|
+
'node': {'ok': False, 'version': None, 'source': None, 'checked_at': None},
|
|
46
|
+
'bun': {'ok': False, 'version': None, 'source': None, 'checked_at': None},
|
|
47
|
+
'optional': {}
|
|
48
|
+
},
|
|
49
|
+
'python_deps': {'ok': True, 'missing': [], 'checked_at': None},
|
|
50
|
+
'satisfied': False,
|
|
51
|
+
'last_doctor': None,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_config(cfg: Dict[str, Any]) -> None:
|
|
56
|
+
p = get_config_path()
|
|
57
|
+
try:
|
|
58
|
+
cfg['last_doctor'] = datetime.utcnow().isoformat() + 'Z'
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
with open(p, 'w', encoding='utf-8') as f:
|
|
62
|
+
json.dump(cfg, f, indent=2)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
from .persist import load_config, save_config
|
|
4
|
+
from .doctor import run_doctor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SKIP_COMMANDS = { 'doctor', 'forcedev' }
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_and_gate(command: str) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Runs once before any CLI command (except 'doctor').
|
|
13
|
+
If environment hasn't satisfied mandatory requirements, invoke doctor.
|
|
14
|
+
After a successful run, future commands proceed without gating.
|
|
15
|
+
"""
|
|
16
|
+
if command in SKIP_COMMANDS:
|
|
17
|
+
return
|
|
18
|
+
# Allow CI to skip interactivity but still fail properly
|
|
19
|
+
if os.getenv('DARS_NO_PREFLIGHT') == '1':
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
cfg = load_config()
|
|
23
|
+
if cfg.get('satisfied'):
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# If not satisfied, run doctor interactively
|
|
27
|
+
code = run_doctor(check_only=False, auto_yes=False, install_all=False, force=False)
|
|
28
|
+
# If doctor returns non-zero, we still persist current state and let the caller decide to exit
|
|
29
|
+
# but generally, the CLI will continue only if requirements are satisfied; we make it strict here
|
|
30
|
+
cfg = load_config()
|
|
31
|
+
if not cfg.get('satisfied'):
|
|
32
|
+
# Propagate an exception to abort command execution
|
|
33
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.prompt import Prompt, Confirm
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
def render_report(node: Dict, bun: Dict, py: Dict, esb: Dict = None, vit: Dict = None):
|
|
10
|
+
table = Table(title="Dars Doctor — Environment Report", box=None)
|
|
11
|
+
table.add_column("Component", style="cyan")
|
|
12
|
+
table.add_column("Required", style="white")
|
|
13
|
+
table.add_column("Detected", style="green")
|
|
14
|
+
table.add_column("Status", style="bold")
|
|
15
|
+
|
|
16
|
+
n_status = "OK" if node.get("ok") else "MISSING"
|
|
17
|
+
b_status = "OK" if bun.get("ok") else "MISSING"
|
|
18
|
+
p_missing = py.get("missing", [])
|
|
19
|
+
p_status = "OK" if not p_missing else f"Missing: {len(p_missing)}"
|
|
20
|
+
|
|
21
|
+
table.add_row("Node.js", "LTS (stable)", node.get("version") or "-", n_status)
|
|
22
|
+
table.add_row("Bun", "Stable", bun.get("version") or "-", b_status)
|
|
23
|
+
table.add_row("Python deps", "pyproject.toml", "-", p_status)
|
|
24
|
+
if esb is not None:
|
|
25
|
+
table.add_row("esbuild", "optional", (esb.get("version") if esb else "-") or "-", "OK" if esb and esb.get("ok") else "MISSING")
|
|
26
|
+
if vit is not None:
|
|
27
|
+
table.add_row("vite", "optional", (vit.get("version") if vit else "-") or "-", "OK" if vit and vit.get("ok") else "MISSING")
|
|
28
|
+
|
|
29
|
+
console.print(table)
|
|
30
|
+
if p_missing:
|
|
31
|
+
bullets = "\n".join([f" • {req}" for req in p_missing])
|
|
32
|
+
console.print(Panel(bullets or "", title="Missing Python packages", border_style="yellow"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def prompt_action(has_missing: bool) -> str:
|
|
36
|
+
console.print(Panel("Select an action", border_style="cyan"))
|
|
37
|
+
if has_missing:
|
|
38
|
+
console.print("[1] Install ALL missing\n[2] Re-run checks\n[3] Quit")
|
|
39
|
+
choices = ["1","2","3"]
|
|
40
|
+
default = "1"
|
|
41
|
+
else:
|
|
42
|
+
console.print("[1] Re-run checks\n[2] Quit")
|
|
43
|
+
choices = ["1","2"]
|
|
44
|
+
default = "1"
|
|
45
|
+
while True:
|
|
46
|
+
choice = Prompt.ask("Choice", choices=choices, default=default)
|
|
47
|
+
return choice
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def confirm_install(summary_lines: List[str]) -> bool:
|
|
51
|
+
console.print(Panel("The following will be installed:", border_style="yellow"))
|
|
52
|
+
for line in summary_lines:
|
|
53
|
+
console.print(f" • {line}")
|
|
54
|
+
return Confirm.ask("Proceed with installation?", default=True)
|