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.
Files changed (135) hide show
  1. {dars_framework-1.2.1/dars_framework.egg-info → dars_framework-1.2.3}/PKG-INFO +3 -3
  2. {dars_framework-1.2.1 → dars_framework-1.2.3}/README.md +4 -1
  3. dars_framework-1.2.3/dars/cli/doctor/__init__.py +1 -0
  4. dars_framework-1.2.3/dars/cli/doctor/detect.py +154 -0
  5. dars_framework-1.2.3/dars/cli/doctor/doctor.py +176 -0
  6. dars_framework-1.2.3/dars/cli/doctor/installers.py +100 -0
  7. dars_framework-1.2.3/dars/cli/doctor/persist.py +62 -0
  8. dars_framework-1.2.3/dars/cli/doctor/preflight.py +33 -0
  9. dars_framework-1.2.3/dars/cli/doctor/ui.py +54 -0
  10. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/main.py +74 -15
  11. dars_framework-1.2.3/dars/core/js_bridge.py +99 -0
  12. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/properties.py +3 -3
  13. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/state.py +54 -0
  14. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/html_css_js.py +24 -1
  15. dars_framework-1.2.3/dars/js_lib.py +206 -0
  16. dars_framework-1.2.3/dars/security.py +195 -0
  17. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/dState/state_mods_demo.py +2 -5
  18. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/version.py +2 -2
  19. {dars_framework-1.2.1 → dars_framework-1.2.3/dars_framework.egg-info}/PKG-INFO +3 -3
  20. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/SOURCES.txt +8 -9
  21. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/requires.txt +1 -1
  22. {dars_framework-1.2.1 → dars_framework-1.2.3}/pyproject.toml +3 -5
  23. dars_framework-1.2.1/dars/js_lib.py +0 -133
  24. dars_framework-1.2.1/dars/security.py +0 -133
  25. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/index.html +0 -72
  26. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/lib/dars.min.js +0 -131
  27. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/runtime_css.css +0 -676
  28. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/runtime_dars.js +0 -349
  29. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/script.js +0 -1
  30. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/snapshot.json +0 -1
  31. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/styles.css +0 -0
  32. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/vdom_tree.js +0 -1
  33. dars_framework-1.2.1/dars/templates/examples/advanced/dState/dars_preview/version.txt +0 -1
  34. {dars_framework-1.2.1 → dars_framework-1.2.3}/LICENSE +0 -0
  35. {dars_framework-1.2.1 → dars_framework-1.2.3}/MANIFEST.in +0 -0
  36. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/__init__.py +0 -0
  37. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/all.py +0 -0
  38. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/__init__.py +0 -0
  39. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/hot_reload.py +0 -0
  40. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/preview.py +0 -0
  41. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/cli/translations.py +0 -0
  42. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/__init__.py +0 -0
  43. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/__init__.py +0 -0
  44. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/accordion.py +0 -0
  45. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/card.py +0 -0
  46. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/modal.py +0 -0
  47. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/navbar.py +0 -0
  48. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/table.py +0 -0
  49. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/advanced/tabs.py +0 -0
  50. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/__init__.py +0 -0
  51. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/button.py +0 -0
  52. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/checkbox.py +0 -0
  53. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/container.py +0 -0
  54. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/datepicker.py +0 -0
  55. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/image.py +0 -0
  56. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/input.py +0 -0
  57. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/link.py +0 -0
  58. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/markdown.py +0 -0
  59. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/page.py +0 -0
  60. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/progressbar.py +0 -0
  61. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/radiobutton.py +0 -0
  62. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/select.py +0 -0
  63. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/slider.py +0 -0
  64. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/spinner.py +0 -0
  65. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/text.py +0 -0
  66. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/textarea.py +0 -0
  67. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/basic/tooltip.py +0 -0
  68. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/__init__.py +0 -0
  69. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/anchor.py +0 -0
  70. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/flex.py +0 -0
  71. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/components/layout/grid.py +0 -0
  72. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/config.py +0 -0
  73. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/__init__.py +0 -0
  74. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/app.py +0 -0
  75. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/component.py +0 -0
  76. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/core/events.py +0 -0
  77. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/apps_test/health_check.py +0 -0
  78. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/run_tests.py +0 -0
  79. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_advanced_components.py +0 -0
  80. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_basic_components.py +0 -0
  81. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_core_and_cli.py +0 -0
  82. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_layout_components.py +0 -0
  83. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/dars_tests/tests/test_version_check.py +0 -0
  84. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/__init__.py +0 -0
  85. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/app.md +0 -0
  86. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/cli.md +0 -0
  87. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/components.md +0 -0
  88. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/custom_components.md +0 -0
  89. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/events.md +0 -0
  90. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/exporters.md +0 -0
  91. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/getting_started.md +0 -0
  92. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/index.md +0 -0
  93. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/scripts.md +0 -0
  94. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/docs/state_management.md +0 -0
  95. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/__init__.py +0 -0
  96. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/base.py +0 -0
  97. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_OLD4.py +0 -0
  98. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_old.py +0 -0
  99. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/OLD/html_css_js_old2.py +0 -0
  100. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/__init__.py +0 -0
  101. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/exporters/web/vdom.py +0 -0
  102. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/__init__.py +0 -0
  103. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/dscript.py +0 -0
  104. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/scripts/script.py +0 -0
  105. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/__init__.py +0 -0
  106. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  107. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/README.md +0 -0
  108. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  109. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +0 -0
  110. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/SimpleDashboard/dashboard.py +0 -0
  111. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +0 -0
  112. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/VariousComponents/all_components_demo.py +0 -0
  113. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/advanced/__init__.py +0 -0
  114. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Forms/form_components.py +0 -0
  115. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Forms/simple_form.py +0 -0
  116. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/HelloWorld/hello_world.py +0 -0
  117. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/flex_layout_responsive.py +0 -0
  118. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/grid_layout_responsive.py +0 -0
  119. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Layouts/layout_multipage_demo.py +0 -0
  120. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/Multipage/multipage_example.py +0 -0
  121. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  122. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  123. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/PWA/pwa_custom_icons.py +0 -0
  124. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/basic/__init__.py +0 -0
  125. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  126. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/demo/complete_app.py +0 -0
  127. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/README.md +0 -0
  128. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +0 -0
  129. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +0 -0
  130. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/examples/markdown/__init__.py +0 -0
  131. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars/templates/html/__init__.py +0 -0
  132. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/dependency_links.txt +0 -0
  133. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/entry_points.txt +0 -0
  134. {dars_framework-1.2.1 → dars_framework-1.2.3}/dars_framework.egg-info/top_level.txt +0 -0
  135. {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.1
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.0.0
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 mor information visit the [Documentation](https://ztamdev.github.io/Dars-Framework/documentation.html)
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)