dars-framework 1.2.3__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.
Files changed (118) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +69 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/doctor/__init__.py +1 -0
  5. dars/cli/doctor/detect.py +154 -0
  6. dars/cli/doctor/doctor.py +176 -0
  7. dars/cli/doctor/installers.py +100 -0
  8. dars/cli/doctor/persist.py +62 -0
  9. dars/cli/doctor/preflight.py +33 -0
  10. dars/cli/doctor/ui.py +54 -0
  11. dars/cli/hot_reload.py +33 -0
  12. dars/cli/main.py +1107 -0
  13. dars/cli/preview.py +448 -0
  14. dars/cli/translations.py +531 -0
  15. dars/components/__init__.py +0 -0
  16. dars/components/advanced/__init__.py +8 -0
  17. dars/components/advanced/accordion.py +26 -0
  18. dars/components/advanced/card.py +33 -0
  19. dars/components/advanced/modal.py +45 -0
  20. dars/components/advanced/navbar.py +44 -0
  21. dars/components/advanced/table.py +25 -0
  22. dars/components/advanced/tabs.py +31 -0
  23. dars/components/basic/__init__.py +34 -0
  24. dars/components/basic/button.py +55 -0
  25. dars/components/basic/checkbox.py +35 -0
  26. dars/components/basic/container.py +29 -0
  27. dars/components/basic/datepicker.py +139 -0
  28. dars/components/basic/image.py +36 -0
  29. dars/components/basic/input.py +57 -0
  30. dars/components/basic/link.py +31 -0
  31. dars/components/basic/markdown.py +86 -0
  32. dars/components/basic/page.py +20 -0
  33. dars/components/basic/progressbar.py +18 -0
  34. dars/components/basic/radiobutton.py +35 -0
  35. dars/components/basic/select.py +82 -0
  36. dars/components/basic/slider.py +63 -0
  37. dars/components/basic/spinner.py +12 -0
  38. dars/components/basic/text.py +23 -0
  39. dars/components/basic/textarea.py +46 -0
  40. dars/components/basic/tooltip.py +19 -0
  41. dars/components/layout/__init__.py +0 -0
  42. dars/components/layout/anchor.py +13 -0
  43. dars/components/layout/flex.py +26 -0
  44. dars/components/layout/grid.py +45 -0
  45. dars/config.py +134 -0
  46. dars/core/__init__.py +0 -0
  47. dars/core/app.py +957 -0
  48. dars/core/component.py +284 -0
  49. dars/core/events.py +102 -0
  50. dars/core/js_bridge.py +99 -0
  51. dars/core/properties.py +127 -0
  52. dars/core/state.py +309 -0
  53. dars/dars_tests/apps_test/health_check.py +56 -0
  54. dars/dars_tests/run_tests.py +275 -0
  55. dars/dars_tests/tests/test_advanced_components.py +69 -0
  56. dars/dars_tests/tests/test_basic_components.py +88 -0
  57. dars/dars_tests/tests/test_core_and_cli.py +17 -0
  58. dars/dars_tests/tests/test_layout_components.py +58 -0
  59. dars/dars_tests/tests/test_version_check.py +21 -0
  60. dars/docs/__init__.py +0 -0
  61. dars/docs/app.md +290 -0
  62. dars/docs/cli.md +80 -0
  63. dars/docs/components.md +1679 -0
  64. dars/docs/custom_components.md +30 -0
  65. dars/docs/events.md +45 -0
  66. dars/docs/exporters.md +162 -0
  67. dars/docs/getting_started.md +79 -0
  68. dars/docs/index.md +18 -0
  69. dars/docs/scripts.md +593 -0
  70. dars/docs/state_management.md +57 -0
  71. dars/exporters/__init__.py +0 -0
  72. dars/exporters/base.py +96 -0
  73. dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
  74. dars/exporters/web/OLD/html_css_js_old.py +1406 -0
  75. dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
  76. dars/exporters/web/__init__.py +0 -0
  77. dars/exporters/web/html_css_js.py +2675 -0
  78. dars/exporters/web/vdom.py +251 -0
  79. dars/js_lib.py +206 -0
  80. dars/scripts/__init__.py +0 -0
  81. dars/scripts/dscript.py +26 -0
  82. dars/scripts/script.py +39 -0
  83. dars/security.py +195 -0
  84. dars/templates/__init__.py +0 -0
  85. dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  86. dars/templates/examples/README.md +4 -0
  87. dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  88. dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
  89. dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
  90. dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
  91. dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
  92. dars/templates/examples/advanced/__init__.py +0 -0
  93. dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
  94. dars/templates/examples/basic/Forms/form_components.py +516 -0
  95. dars/templates/examples/basic/Forms/simple_form.py +379 -0
  96. dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
  97. dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
  98. dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
  99. dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
  100. dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
  101. dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  102. dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  103. dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
  104. dars/templates/examples/basic/__init__.py +0 -0
  105. dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  106. dars/templates/examples/demo/complete_app.py +21 -0
  107. dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
  108. dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
  109. dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
  110. dars/templates/examples/markdown/__init__.py +0 -0
  111. dars/templates/html/__init__.py +0 -0
  112. dars/version.py +2 -0
  113. dars_framework-1.2.3.dist-info/METADATA +15 -0
  114. dars_framework-1.2.3.dist-info/RECORD +118 -0
  115. dars_framework-1.2.3.dist-info/WHEEL +5 -0
  116. dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
  117. dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
  118. dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/__init__.py ADDED
File without changes
dars/all.py ADDED
@@ -0,0 +1,69 @@
1
+ # Barrel import for all Dars components and core modules
2
+ # Usage: from dars.all import *
3
+
4
+ # Core
5
+ from dars.core.app import App
6
+ from dars.core.component import Component
7
+ from dars.core.events import EventManager
8
+ from dars.core.properties import *
9
+
10
+ # Basic Components
11
+ from dars.components.basic.button import Button
12
+ from dars.components.basic.checkbox import Checkbox
13
+ from dars.components.basic.container import Container
14
+ from dars.components.basic.datepicker import DatePicker
15
+ from dars.components.basic.image import Image
16
+ from dars.components.basic.input import Input
17
+ from dars.components.basic.link import Link
18
+ from dars.components.basic.page import Page
19
+ from dars.components.basic.progressbar import ProgressBar
20
+ from dars.components.basic.radiobutton import RadioButton
21
+ from dars.components.basic.select import Select
22
+ from dars.components.basic.slider import Slider
23
+ from dars.components.basic.spinner import Spinner
24
+ from dars.components.basic.text import Text
25
+ from dars.components.basic.textarea import Textarea
26
+ from dars.components.basic.tooltip import Tooltip
27
+ from dars.components.basic.markdown import Markdown
28
+
29
+ # Advanced Components
30
+ from dars.components.advanced.accordion import Accordion
31
+ from dars.components.advanced.card import Card
32
+ from dars.components.advanced.modal import Modal
33
+ from dars.components.advanced.navbar import Navbar
34
+ from dars.components.advanced.table import Table
35
+ from dars.components.advanced.tabs import Tabs
36
+
37
+
38
+ # Layout
39
+ from dars.components.layout.grid import GridLayout, LayoutBase
40
+ from dars.components.layout.flex import FlexLayout
41
+ from dars.components.layout.anchor import AnchorPoint
42
+
43
+ from dars.scripts.script import *
44
+ from dars.scripts.dscript import dScript
45
+
46
+ # Exporters (optional, for direct use)
47
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
48
+ from dars.core.events import EventTypes
49
+ from dars.core.events import EventHandler, EventEmitter, EventManager
50
+
51
+ from dars.version import __version__
52
+ from dars.dars_tests import *
53
+ from dars.dars_tests.run_tests import run_app_tests, run_unit_tests, main
54
+ # CLI (optional, for advanced usage)
55
+ # from dars.cli.main import main as dars_cli_main
56
+ from dars.core.state import dState, Mod
57
+
58
+ __all__ = [
59
+ 'App', 'Component', 'EventManager',
60
+ 'Button', 'Checkbox', 'Container', 'DatePicker', 'Image', 'Input', 'Link', 'Page', 'ProgressBar',
61
+ 'RadioButton', 'Select', 'Slider', 'Spinner', 'Text', 'Textarea', 'Tooltip',
62
+ 'Accordion', 'Card', 'Modal', 'Navbar', 'Table', 'Tabs',
63
+ 'GridLayout', 'FlexLayout', 'LayoutBase', 'AnchorPoint',
64
+ 'InlineScript', 'FileScript', 'dScript', 'HTMLCSSJSExporter',
65
+ 'EventTypes', 'EventHandler', 'EventEmitter', 'EventManager', 'Markdown',
66
+ '__version__',
67
+ 'run_app_tests', 'run_unit_tests', 'main',
68
+ 'dState', 'Mod',
69
+ ]
dars/cli/__init__.py ADDED
File without changes
@@ -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)
dars/cli/doctor/ui.py ADDED
@@ -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)