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/cli/main.py ADDED
@@ -0,0 +1,1107 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dars Exporter - Command line tool for exporting Dars applications
4
+ """
5
+ import shutil
6
+ import subprocess
7
+ import venv
8
+ from rich.prompt import Confirm
9
+ from rich.syntax import Syntax
10
+ import argparse
11
+ import os
12
+ import sys
13
+ import time
14
+ import importlib.util
15
+ from pathlib import Path
16
+ from typing import Optional, Dict, Any
17
+
18
+ from rich.console import Console
19
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
20
+ from rich.panel import Panel
21
+ from rich.text import Text
22
+ from rich.table import Table
23
+ from rich.markdown import Markdown
24
+ from rich import print as rprint
25
+ from importlib import resources
26
+
27
+ # Importar exportadores
28
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
29
+
30
+ from dars.core.app import App
31
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
32
+ from dars.cli.translations import translator
33
+ from dars.config import load_config, resolve_paths, write_default_config, update_config
34
+ from dars.cli.doctor.preflight import check_and_gate
35
+ from dars.cli.doctor.doctor import run_doctor, run_forcedev
36
+
37
+ console = Console()
38
+
39
+ class RichHelpFormatter(argparse.HelpFormatter):
40
+ """Custom formatter for argparse help using Rich"""
41
+
42
+ def __init__(self, prog, indent_increment=2, max_help_position=24, width=None):
43
+ super().__init__(prog, indent_increment, max_help_position, width)
44
+
45
+ def format_help(self):
46
+ # Call the original method to get the help text
47
+ help_text = super().format_help()
48
+ return help_text
49
+
50
+ def add_text(self, text):
51
+ # Override this method to prevent the epilog from being shown in the options section
52
+ if text and (text.startswith('\nEjemplos de uso:') or text.startswith('\nUsage examples:')):
53
+ return
54
+ return super().add_text(text)
55
+
56
+ def _format_action(self, action):
57
+ # Check if this is the help action and replace its help message with the translated one
58
+ if action.option_strings and ('-h' in action.option_strings or '--help' in action.option_strings):
59
+ action.help = translator.get('help_arg_message')
60
+ return super()._format_action(action)
61
+
62
+ @classmethod
63
+ def rich_print_help(cls, parser, console=console):
64
+ # Get the standard help text
65
+ help_text = parser.format_help()
66
+
67
+ # Extract the main sections
68
+ sections = {}
69
+ current_section = None
70
+ lines = help_text.split('\n')
71
+ section_content = []
72
+
73
+ for line in lines:
74
+ if line and not line.startswith(' ') and line.endswith(':'):
75
+ # It's a section header
76
+ if current_section:
77
+ sections[current_section] = '\n'.join(section_content)
78
+ current_section = line[:-1] # Remove the colon
79
+ section_content = []
80
+ elif current_section:
81
+ section_content.append(line)
82
+
83
+ # Add the last section
84
+ if current_section and section_content:
85
+ sections[current_section] = '\n'.join(section_content)
86
+
87
+ # Show the program title
88
+ prog_name = parser.prog
89
+ description = parser.description
90
+
91
+ # Main panel
92
+ console.print(Panel(
93
+ Text(prog_name, style="bold cyan", justify="center"),
94
+ subtitle=translator.get('cli_subtitle'),
95
+ border_style="cyan"
96
+ ))
97
+
98
+ # Check if there are examples in the epilog
99
+ epilog_content = ""
100
+ if parser.epilog:
101
+ epilog_content = parser.epilog.strip()
102
+
103
+ # Show each section with style
104
+ for section, content in sections.items():
105
+ if section == 'usage':
106
+ # Usage section
107
+ usage = content.strip()
108
+ console.print(f"\n[bold cyan]{translator.get('usage')}:[/bold cyan]")
109
+ console.print(Syntax(usage, "bash", theme="monokai", word_wrap=True))
110
+ elif 'positional arguments' in section.lower():
111
+ # Positional arguments
112
+ console.print(f"\n[bold cyan]{translator.get('positional_arguments')}:[/bold cyan]")
113
+ _print_arguments_table(content)
114
+ elif 'optional arguments' in section.lower() or 'options' in section.lower():
115
+ # Optional arguments
116
+ console.print(f"\n[bold cyan]{translator.get('options')}:[/bold cyan]")
117
+ _print_arguments_table(content)
118
+ elif section.lower() == 'commands' or 'subcommands' in section.lower():
119
+ # Subcommands
120
+ console.print(f"\n[bold cyan]{translator.get('commands')}:[/bold cyan]")
121
+ _print_arguments_table(content)
122
+ elif 'examples' in section.lower() or section.lower() == 'epilog':
123
+ # We don't process examples here to avoid duplication
124
+ pass
125
+ elif section.lower() != 'usage examples':
126
+ # Other sections (skip 'usage examples' to avoid duplication)
127
+ console.print(f"\n[bold cyan]{section.upper()}:[/bold cyan]")
128
+ console.print(content.strip())
129
+
130
+ # Always show examples at the end
131
+ console.print(f"\n[bold cyan]{translator.get('examples')}:[/bold cyan]")
132
+ # Get the actual examples from translations
133
+ examples_text = translator.get('examples_text')
134
+ examples = [line.strip() for line in examples_text.strip().split('\n') if line.strip()]
135
+
136
+ examples_table = Table(box=None, expand=True, show_header=False, padding=(0, 1, 0, 1))
137
+ examples_table.add_column("Example", overflow="fold")
138
+
139
+ for example in examples:
140
+ if example.strip():
141
+ examples_table.add_row(Syntax(example.strip(), "bash", theme="monokai"))
142
+
143
+ console.print(Panel(examples_table, border_style="cyan", padding=(1, 2)))
144
+
145
+ def pretty_print_help(parser: argparse.ArgumentParser) -> None:
146
+ # Just print argparse help (no custom header)
147
+ parser.print_help()
148
+
149
+ def _print_arguments_table(content):
150
+ """Prints a table of arguments from the text content"""
151
+ table = Table(show_header=False, box=None, padding=(0, 2, 0, 0), expand=True)
152
+ table.add_column(translator.get('argument_column'), style="bold green", width=30, no_wrap=True)
153
+ table.add_column(translator.get('description_column'), style="dim white", overflow="fold")
154
+
155
+ lines = content.strip().split('\n')
156
+ current_arg = None
157
+ current_desc = []
158
+
159
+ for line in lines:
160
+ if line.strip():
161
+ if not line.startswith(' '):
162
+ # Es un nuevo argumento
163
+ if current_arg:
164
+ # Estilizar el argumento
165
+ styled_arg = current_arg
166
+ if '-' in styled_arg:
167
+ # Resaltar las opciones cortas y largas
168
+ parts = styled_arg.split(', ')
169
+ styled_parts = []
170
+ for part in parts:
171
+ if part.startswith('--'):
172
+ styled_parts.append(f"[cyan]{part}[/cyan]")
173
+ elif part.startswith('-'):
174
+ styled_parts.append(f"[green]{part}[/green]")
175
+ else:
176
+ styled_parts.append(part)
177
+ styled_arg = ", ".join(styled_parts)
178
+
179
+ table.add_row(styled_arg, '\n'.join(current_desc))
180
+
181
+ parts = line.strip().split(' ', 1)
182
+ current_arg = parts[0].strip()
183
+ current_desc = [parts[1].strip()] if len(parts) > 1 else []
184
+ else:
185
+ # Es continuación de la descripción
186
+ current_desc.append(line.strip())
187
+
188
+ # Añadir el último argumento
189
+ if current_arg:
190
+ # Estilizar el último argumento
191
+ styled_arg = current_arg
192
+ if '-' in styled_arg:
193
+ # Resaltar las opciones cortas y largas
194
+ parts = styled_arg.split(', ')
195
+ styled_parts = []
196
+ for part in parts:
197
+ if part.startswith('--'):
198
+ styled_parts.append(f"[cyan]{part}[/cyan]")
199
+ elif part.startswith('-'):
200
+ styled_parts.append(f"[green]{part}[/green]")
201
+ else:
202
+ styled_parts.append(part)
203
+ styled_arg = ", ".join(styled_parts)
204
+
205
+ table.add_row(styled_arg, '\n'.join(current_desc))
206
+
207
+ console.print(table)
208
+
209
+ class DarsExporter:
210
+ """Exportador principal de Dars"""
211
+
212
+ def __init__(self):
213
+ self.exporters = {
214
+ 'html': HTMLCSSJSExporter()
215
+ }
216
+
217
+ def load_app_from_file(self, file_path: str) -> Optional[App]:
218
+ """Loads a Dars application from a Python file"""
219
+ try:
220
+ # Verify that the file exists
221
+ if not os.path.exists(file_path):
222
+ console.print(f"[red]{translator.get('error_file_not_exists')} {file_path}[/red]")
223
+ return None
224
+
225
+ # Add the application's root directory to sys.path
226
+ file_dir = os.path.dirname(os.path.abspath(file_path))
227
+ if file_dir not in sys.path:
228
+ sys.path.insert(0, file_dir)
229
+
230
+ # Load the module dynamically
231
+ spec = importlib.util.spec_from_file_location("user_app", file_path)
232
+ if spec is None or spec.loader is None:
233
+ console.print(f"[red]{translator.get('error_file_load')} {file_path}[/red]")
234
+ return None
235
+
236
+ module = importlib.util.module_from_spec(spec)
237
+ spec.loader.exec_module(module)
238
+
239
+ # Look for the 'app' variable in the module
240
+ if hasattr(module, 'app') and isinstance(module.app, App):
241
+ return module.app
242
+ else:
243
+ console.print(f"[red]{translator.get('error_no_app_var')} {file_path}[/red]")
244
+ return None
245
+
246
+ except Exception as e:
247
+ console.print(f"[red]{translator.get('error_loading_file')}: {e}[/red]")
248
+ return None
249
+
250
+ def validate_app(self, app: App) -> bool:
251
+ """Validates a Dars application"""
252
+ errors = app.validate()
253
+
254
+ if errors:
255
+ console.print(f"[red]{translator.get('validation_errors')}[/red]")
256
+ for error in errors:
257
+ console.print(f" • {error}")
258
+ return False
259
+
260
+ return True
261
+
262
+ def export_app(self, app: App, format_name: str, output_path: str, show_preview: bool = False) -> bool:
263
+ """Exports an application to the specified format"""
264
+
265
+ if format_name not in self.exporters:
266
+ console.print(f"[red]{translator.get('error_format_not_supported')} '{format_name}'[/red]")
267
+ self.show_supported_formats()
268
+ return False
269
+
270
+ exporter = self.exporters[format_name]
271
+
272
+ with Progress(
273
+ SpinnerColumn(),
274
+ TextColumn("[progress.description]{task.description}"),
275
+ BarColumn(),
276
+ TaskProgressColumn(),
277
+ console=console
278
+ ) as progress:
279
+
280
+ # Validation task
281
+ task1 = progress.add_task(translator.get('validating_app'), total=100)
282
+ progress.update(task1, advance=30)
283
+
284
+ if not self.validate_app(app):
285
+ progress.update(task1, completed=100)
286
+ return False
287
+
288
+ progress.update(task1, advance=70)
289
+
290
+ # Export task
291
+ task2 = progress.add_task(f"{translator.get('exporting_to')} {format_name}...", total=100)
292
+ progress.update(task2, advance=20)
293
+
294
+ try:
295
+ # En CLI 'dars export', generamos un bundle final (sin hot-reload dev)
296
+ success = exporter.export(app, output_path, bundle=True)
297
+ progress.update(task2, advance=80)
298
+
299
+ if success:
300
+ # Minification step for bundle
301
+ try:
302
+ from dars.security import minify_output_dir
303
+ # Use actual file count progress
304
+ task3 = progress.add_task("Applying minification (bundle)", total=1)
305
+ totals = {"total": 1, "inited": False}
306
+ def _cb(done, total):
307
+ # Initialize task total once when known
308
+ if not totals["inited"] and total > 0:
309
+ progress.update(task3, total=total)
310
+ totals["total"] = total
311
+ totals["inited"] = True
312
+ progress.update(task3, completed=done)
313
+ _ = minify_output_dir(output_path, progress_cb=_cb)
314
+ # Ensure completed
315
+ progress.update(task3, completed=totals.get("total", 1))
316
+ except Exception:
317
+ # Do not fail export on minification errors
318
+ pass
319
+ progress.update(task1, completed=100)
320
+ progress.update(task2, completed=100)
321
+
322
+ # Show success information
323
+ self.show_export_success(app, format_name, output_path)
324
+
325
+ if show_preview and format_name == 'html':
326
+ self.show_preview_info(output_path)
327
+
328
+ return True
329
+ else:
330
+ console.print(f"[red]{translator.get('error_during_export')} {format_name}[/red]")
331
+ return False
332
+
333
+ except Exception as e:
334
+ console.print(f"[red]{translator.get('error_during_export_exception')}: {e}[/red]")
335
+ return False
336
+
337
+ def show_supported_formats(self):
338
+ """Shows supported formats"""
339
+ table = Table(title=translator.get('supported_export_formats'))
340
+ table.add_column(translator.get('format_name'), style="cyan")
341
+ table.add_column(translator.get('format_description'), style="white")
342
+ table.add_column(translator.get('html_description'), style="green")
343
+
344
+ formats_info = {
345
+ 'html': ('HTML/CSS/JavaScript', 'Web'),
346
+ }
347
+
348
+ for format_name, (description, platform) in formats_info.items():
349
+ table.add_row(format_name, description, platform)
350
+
351
+ console.print(table)
352
+
353
+ def show_export_success(self, app: App, format_name: str, output_path: str):
354
+ """Shows export success information"""
355
+ stats = app.get_stats()
356
+
357
+ panel_content = f"""
358
+ [green]✓[/green] {translator.get('export_completed_successfully')}
359
+
360
+ [bold]{translator.get('application')}:[/bold] {app.title}
361
+ [bold]{translator.get('format')}:[/bold] {format_name}
362
+ [bold]{translator.get('output_directory')}:[/bold] {output_path}
363
+
364
+ [bold]{translator.get('statistics')}:[/bold]
365
+ • {translator.get('total_components')}: {stats['total_components']}
366
+ • {translator.get('total_pages')}: {stats.get('total_pages', 1)}
367
+ • {translator.get('max_depth')}: {stats['max_depth']}
368
+ • {translator.get('scripts')}: {stats['scripts_count']}
369
+ • {translator.get('global_styles')}: {stats['global_styles_count']}
370
+ """
371
+
372
+ console.print(Panel(panel_content, title=translator.get('export_successful'), border_style="green"))
373
+
374
+ def show_preview_info(self, output_path: str):
375
+ """Shows information about how to preview the application"""
376
+ index_path = os.path.join(output_path, "index.html")
377
+
378
+ if os.path.exists(index_path):
379
+ console.print(f"\n[bold cyan]{translator.get('to_preview_app')}:[/bold cyan]")
380
+ console.print(f" {translator.get('open_in_browser')}: file://{os.path.abspath(index_path)}")
381
+ console.print(f" {translator.get('or_use')}: dars preview {output_path}")
382
+
383
+ def show_app_info(self, app: App):
384
+ """Shows detailed information about the application"""
385
+ stats = app.get_stats()
386
+
387
+ # Basic information
388
+ info_table = Table(title=f"{translator.get('app_information')}: {app.title}")
389
+ info_table.add_column(translator.get('property_column'), style="cyan")
390
+ info_table.add_column(translator.get('value_column'), style="white")
391
+
392
+ info_table.add_row(translator.get('title'), app.title)
393
+ info_table.add_row(translator.get('total_components'), str(stats['total_components']))
394
+ info_table.add_row(translator.get('max_depth'), str(stats['max_depth']))
395
+ info_table.add_row(translator.get('scripts'), str(stats['scripts_count']))
396
+ info_table.add_row(translator.get('global_styles'), str(stats['global_styles_count']))
397
+ info_table.add_row(translator.get('theme'), app.config.get('theme', 'light'))
398
+
399
+ console.print(info_table)
400
+
401
+ # Component tree
402
+ if app.root:
403
+ console.print(f"\n[bold]{translator.get('component_structure')}:[/bold]")
404
+ self.print_component_tree(app.root)
405
+
406
+ def print_component_tree(self, component, level: int = 0):
407
+ """Prints the component tree"""
408
+ indent = " " * level
409
+ component_name = component.__class__.__name__
410
+ component_id = f" (id: {component.id})" if component.id else ""
411
+
412
+ console.print(f"{indent}├─ {component_name}{component_id}")
413
+
414
+ for child in component.children:
415
+ self.print_component_tree(child, level + 1)
416
+
417
+
418
+ def init_project(self, name: str, template: Optional[str] = None):
419
+ """Initializes a base Dars project, optionally using a template"""
420
+ if os.path.exists(name):
421
+ console.print(f"[red]❌ {translator.get('directory_exists').format(name=name)}[/red]")
422
+ return
423
+
424
+ # Create project directory
425
+ os.makedirs(name)
426
+ console.print(f"[green]✔ {translator.get('directory_created').format(name=name)}[/green]")
427
+
428
+ if template:
429
+ # Get template information
430
+ templates = list_templates()
431
+ if template not in templates:
432
+ console.print(f"[red]❌ {translator.get('template_not_found').format(template=template)}[/red]")
433
+ return
434
+
435
+ template_info = templates[template]
436
+ template_dir = template_info['template_dir']
437
+ extra_files = template_info['extra_files']
438
+
439
+ if not extra_files:
440
+ console.print(f"[yellow]⚠ {translator.get('template_empty').format(template=template)}[/yellow]")
441
+ return
442
+
443
+ # Copy ALL files (no main_file anymore)
444
+ for extra_file in extra_files:
445
+ src_file = template_dir / extra_file
446
+ dest_file = os.path.join(name, extra_file)
447
+
448
+ # Create directories if needed
449
+ os.makedirs(os.path.dirname(dest_file), exist_ok=True)
450
+
451
+ if src_file.exists():
452
+ shutil.copy2(src_file, dest_file)
453
+ console.print(f"[green]✔ {translator.get('extra_file_copied').format(file=extra_file)}[/green]")
454
+
455
+ console.print(f"[green]✔ {translator.get('template_copied').format(template=template)}[/green]")
456
+ else:
457
+ # Default hello world code (sin template)
458
+ HELLO_WORLD_CODE = """
459
+ from dars.all import *
460
+
461
+ app = App(title="Hello World", theme="dark")
462
+ # Crear componentes
463
+ index = Page(
464
+ Text(
465
+ text="Hello World",
466
+ style={
467
+ 'font-size': '48px',
468
+ 'color': '#2c3e50',
469
+ 'margin-bottom': '20px',
470
+ 'font-weight': 'bold',
471
+ 'text-align': 'center'
472
+ }
473
+ ),
474
+ Text(
475
+ text="Hello World",
476
+ style={
477
+ 'font-size': '20px',
478
+ 'color': '#7f8c8d',
479
+ 'margin-bottom': '40px',
480
+ 'text-align': 'center'
481
+ }
482
+ ),
483
+
484
+ Button(
485
+ text="Click Me!",
486
+ on_click= dScript("alert('Hello World')"),
487
+ on_mouse_enter=dScript("this.style.backgroundColor = '#2980b9';"),
488
+ on_mouse_leave=dScript("this.style.backgroundColor = '#3498db';"),
489
+ style={
490
+ 'background-color': '#3498db',
491
+ 'color': 'white',
492
+ 'padding': '15px 30px',
493
+ 'border': 'none',
494
+ 'border-radius': '8px',
495
+ 'font-size': '18px',
496
+ 'cursor': 'pointer',
497
+ 'transition': 'background-color 0.3s'
498
+ }
499
+ ),
500
+ style={
501
+ 'display': 'flex',
502
+ 'flex-direction': 'column',
503
+ 'align-items': 'center',
504
+ 'justify-content': 'center',
505
+ 'min-height': '100vh',
506
+ 'background-color': '#f0f2f5',
507
+ 'font-family': 'Arial, sans-serif'
508
+ }
509
+ )
510
+
511
+ app.add_page("index", index, title="Hello World", index=True)
512
+
513
+ if __name__ == "__main__":
514
+ app.rTimeCompile()
515
+ """
516
+ main_py = Path(name) / "main.py"
517
+ main_py.write_text(HELLO_WORLD_CODE.strip(), encoding="utf-8")
518
+ console.print(f"[green]✔ {translator.get('main_py_created')}[/green]")
519
+
520
+ # Create default dars.config.json for the new project
521
+ try:
522
+ write_default_config(os.path.abspath(name), overwrite=False)
523
+ console.print("[green]✔ dars.config.json created[/green]")
524
+ except Exception:
525
+ # Non-fatal; keep init working even if config write fails
526
+ pass
527
+
528
+ # Final instructions
529
+ console.print(f"\n[bold cyan]🎉 {translator.get('project_initialized')}[/bold cyan]")
530
+ console.print(Syntax(f"cd {name}", "bash"))
531
+ console.print(Syntax(f"\n{translator.get('export_command')}:", "bash"))
532
+ console.print(Syntax(f"dars export (python file) --format html --output build", "bash"))
533
+ console.print(Syntax(f"\n{translator.get('preview_command')}:", "bash"))
534
+ console.print(Syntax(f"python (python file)", "bash"))
535
+
536
+ def print_version_info():
537
+ import importlib.util
538
+ import os
539
+ from rich.panel import Panel
540
+ from rich.console import Console
541
+ console = Console()
542
+ version_path = os.path.join(os.path.dirname(__file__), '../version.py')
543
+ spec = importlib.util.spec_from_file_location("dars.version", version_path)
544
+ version_mod = importlib.util.module_from_spec(spec)
545
+ spec.loader.exec_module(version_mod)
546
+ version = getattr(version_mod, "__version__", "unknown")
547
+ release_url = getattr(version_mod, "__release_url__", "https://github.com/ZtaMDev/Dars-Framework/releases")
548
+ panel_content = f"[bold cyan]Dars Framework[/bold cyan]\n\n[green]Version:[/green] {version}\n[green]Release notes:[/green] [link={release_url}]{release_url}[/link]"
549
+ console.print(Panel(panel_content, title="Dars Version", border_style="cyan"))
550
+
551
+ def create_parser(include_hidden: bool = True) -> argparse.ArgumentParser:
552
+ """Creates the command line argument parser"""
553
+ parser = argparse.ArgumentParser(
554
+ description=translator.get('main_description'),
555
+ formatter_class=argparse.HelpFormatter,
556
+ epilog=""
557
+ )
558
+ parser.add_argument('-v', '--version', action='store_true', help='Show Dars version and release link')
559
+
560
+ # English-only: no language flag
561
+
562
+ subparsers = parser.add_subparsers(
563
+ dest='command',
564
+ help=translator.get('available_commands'),
565
+ metavar='{export,info,formats,preview,init,build,config,dev,doctor}'
566
+ )
567
+
568
+ # Export command
569
+ export_parser = subparsers.add_parser('export', help=translator.get('export_help'))
570
+ export_parser.add_argument('file', help=translator.get('file_help'))
571
+
572
+ # --format opcional (default: html)
573
+ export_parser.add_argument(
574
+ '--format', '-f',
575
+ choices=["html"],
576
+ default="html",
577
+ help=translator.get('format_help') + " (default: html)"
578
+ )
579
+
580
+ # --output opcional (default: ./dist)
581
+ export_parser.add_argument(
582
+ '--output', '-o',
583
+ default="./dist",
584
+ help=translator.get('output_help') + " (default: ./dist)"
585
+ )
586
+
587
+ export_parser.add_argument('--preview', '-p', action='store_true',
588
+ help=translator.get('preview_help'))
589
+
590
+
591
+ # Info command
592
+ info_parser = subparsers.add_parser('info', help=translator.get('info_help'))
593
+ info_parser.add_argument('file', help=translator.get('file_help'))
594
+
595
+ # Formats command
596
+ formats_parser = subparsers.add_parser('formats', help=translator.get('formats_help'))
597
+
598
+ # Preview command
599
+ preview_parser = subparsers.add_parser('preview', help=translator.get('preview_cmd_help'))
600
+ preview_parser.add_argument('path', help=translator.get('path_help'))
601
+
602
+ init_parser = subparsers.add_parser('init', help=translator.get('init_help'))
603
+ init_parser.add_argument('name', nargs='?', help=translator.get('name_help'))
604
+ init_parser.add_argument(
605
+ '--list-templates', '-L', # Cambia -l por -L
606
+ action='store_true',
607
+ help=translator.get('list_templates_help')
608
+ )
609
+ init_parser.add_argument(
610
+ '--template', '-t',
611
+ help=translator.get('template_help')
612
+ )
613
+ init_parser.add_argument(
614
+ '--update', '-u',
615
+ action='store_true',
616
+ help='Create or update dars.config.json in the target (or current) directory'
617
+ )
618
+
619
+ # Build command (config-driven)
620
+ build_parser = subparsers.add_parser('build', help='Build using dars.config.json')
621
+ build_parser.add_argument(
622
+ '--project', '-p', default='.', help='Project root where dars.config.json resides (default: .)'
623
+ )
624
+
625
+ # Config command (validate)
626
+ config_parser = subparsers.add_parser('config', help='Manage and validate dars.config.json')
627
+ cfg_subparsers = config_parser.add_subparsers(dest='config_command')
628
+ cfg_validate = cfg_subparsers.add_parser('validate', help='Validate dars.config.json in a project')
629
+ cfg_validate.add_argument('--project', '-p', default='.', help='Project root (default: .)')
630
+
631
+ # Dev command (run entry in dev mode)
632
+ dev_parser = subparsers.add_parser('dev', help='Run the configured entry file in development mode')
633
+ dev_parser.add_argument('--project', '-p', default='.', help='Project root where dars.config.json resides (default: .)')
634
+ # English-only: no language option on subparsers
635
+
636
+ # Doctor command
637
+ doctor_parser = subparsers.add_parser('doctor', help='Check and install required external tools (Node LTS, Bun) and Python deps')
638
+ doctor_parser.add_argument('--check', action='store_true', help='Only verify environment and exit non-zero if missing')
639
+ doctor_parser.add_argument('--yes', '-y', action='store_true', help='Assume yes for all prompts')
640
+ doctor_parser.add_argument('--all', action='store_true', help='Install all missing items (with --yes for non-interactive)')
641
+ doctor_parser.add_argument('--force', action='store_true', help='Re-run checks even if environment was previously satisfied')
642
+
643
+ # Hidden forced installer (conditionally added to avoid appearing in help)
644
+ if include_hidden:
645
+ forcedev_parser = subparsers.add_parser('forcedev', help=argparse.SUPPRESS)
646
+
647
+ return parser
648
+
649
+ from pathlib import Path
650
+ from typing import Dict
651
+
652
+ from pathlib import Path
653
+ from typing import Dict
654
+
655
+ from pathlib import Path
656
+ from typing import Dict
657
+
658
+ from pathlib import Path
659
+ from typing import Dict
660
+
661
+ from pathlib import Path
662
+ from typing import Dict
663
+
664
+ from pathlib import Path
665
+ from typing import Dict
666
+
667
+ def list_templates(debug: bool = False) -> Dict[str, Dict]:
668
+ """
669
+ Descubre templates:
670
+ - ignora dirs en IGNORED_DIRS (ej: __pycache__, .git, node_modules)
671
+ - ignora extensiones compiladas ('.pyc', '.pyo', '.pyd')
672
+ - ignora solo archivos ocultos que empiezan con '.' (ej: .env)
673
+ - incluye TODOS los demás archivos ('.py', '.md', '.png', '.json', etc.)
674
+ - salida determinista (ordenada)
675
+ """
676
+ current_file = Path(__file__).resolve()
677
+ templates_base = current_file.parent.parent / "templates" / "examples"
678
+
679
+ if not templates_base.exists():
680
+ # usa console.print si tienes rich.console; aquí dejo print para compatibilidad
681
+ print(f"[red]Error: Template directory not found: {templates_base}[/red]")
682
+ return {}
683
+
684
+ IGNORED_DIRS = {'__pycache__', '.git', '.venv', 'node_modules', '.pytest_cache'}
685
+ IGNORE_EXTS = {'.pyc', '.pyo', '.pyd'}
686
+
687
+ templates: Dict[str, Dict] = {}
688
+
689
+ for category_dir in sorted(templates_base.iterdir()):
690
+ if not (category_dir.is_dir() and not category_dir.name.startswith('__')):
691
+ continue
692
+
693
+ for template_dir in sorted(category_dir.iterdir()):
694
+ if not (template_dir.is_dir() and not template_dir.name.startswith('__')):
695
+ continue
696
+
697
+ found_files = []
698
+ for file_path in sorted(template_dir.rglob('*')):
699
+ # 1) archivo
700
+ if not file_path.is_file():
701
+ if debug: print(f"SKIP (not file): {file_path}")
702
+ continue
703
+
704
+ # 2) si alguna parte del path es una carpeta ignorada
705
+ intersect = set(file_path.parts) & IGNORED_DIRS
706
+ if intersect:
707
+ if debug: print(f"SKIP (ignored dir {intersect}): {file_path}")
708
+ continue
709
+
710
+ # 3) extensiones compiladas
711
+ if file_path.suffix.lower() in IGNORE_EXTS:
712
+ if debug: print(f"SKIP (ignored ext): {file_path}")
713
+ continue
714
+
715
+ # 4) solo ocultos que empiezan con '.' (por ejemplo .gitignore, .env)
716
+ if file_path.name.startswith('.'):
717
+ if debug: print(f"SKIP (hidden file): {file_path}")
718
+ continue
719
+
720
+ # si pasó todos los filtros, lo guardamos (ruta relativa al template)
721
+ rel = str(file_path.relative_to(template_dir))
722
+ if debug: print(f"INCLUDE: {rel}")
723
+ found_files.append(rel)
724
+
725
+ found_files = sorted(found_files)
726
+
727
+ template_key = f"{category_dir.name}/{template_dir.name}"
728
+ templates[template_key] = {
729
+ 'main_file': None, # ya no usamos main_file
730
+ 'extra_files': found_files,
731
+ 'category': category_dir.name,
732
+ 'template_dir': template_dir,
733
+ 'all_files': found_files
734
+ }
735
+
736
+ return templates
737
+
738
+
739
+
740
+
741
+ def list_templates_detailed():
742
+ """Muestra información detallada de los templates disponibles"""
743
+ templates = list_templates()
744
+
745
+ if not templates:
746
+ console.print("[yellow]No templates found[/yellow]")
747
+ return
748
+
749
+ table = Table(title="Available Templates")
750
+ table.add_column("Template", style="cyan")
751
+ table.add_column("Category", style="green")
752
+ table.add_column("Extra Files", style="white")
753
+ table.add_column("Description", style="dim")
754
+
755
+ for template_name, template_info in templates.items():
756
+ extra_files = ", ".join(template_info['extra_files']) if template_info['extra_files'] else "None"
757
+ table.add_row(
758
+ template_name,
759
+ template_info['category'],
760
+ extra_files,
761
+ f"Template with {len(template_info['extra_files'])} extra files"
762
+ )
763
+
764
+ console.print(table)
765
+ def main():
766
+ """Main CLI function"""
767
+ # English-only: no language parameter pre-scan
768
+
769
+ # Intercept only when no args provided; otherwise let argparse show the correct subcommand help
770
+ if len(sys.argv) == 1:
771
+ parser = create_parser(include_hidden=False)
772
+ pretty_print_help(parser)
773
+ return
774
+
775
+ # Continue with normal flow if not help
776
+ # If user asked for top-level help (no subcommand), build parser without hidden commands
777
+ known_cmds = ['export','info','formats','preview','init','build','config','dev','doctor']
778
+ top_level_help = ('-h' in sys.argv or '--help' in sys.argv) and not any(cmd in sys.argv for cmd in known_cmds)
779
+ parser = create_parser(include_hidden=not top_level_help)
780
+ if top_level_help:
781
+ pretty_print_help(create_parser(include_hidden=False))
782
+ return
783
+ args = parser.parse_args()
784
+
785
+ # Set language from args only if explicitly provided
786
+ # This is already handled in the pre-parsing step above, so we don't need to do it again
787
+ # The translator will already have the correct language set
788
+
789
+ # Show version and exit if -v/--version is passed
790
+ if getattr(args, 'version', False):
791
+ print_version_info()
792
+ sys.exit(0)
793
+
794
+ # No banner for normal commands; keep output minimal
795
+
796
+ exporter = DarsExporter()
797
+
798
+ # Run preflight gating for all commands except 'doctor'
799
+ if getattr(args, 'command', None) and args.command != 'doctor':
800
+ try:
801
+ check_and_gate(args.command)
802
+ except SystemExit as e:
803
+ # If doctor failed or user cancelled, abort the command
804
+ sys.exit(e.code if isinstance(e.code, int) else 1)
805
+
806
+ if args.command == 'export':
807
+ # If file points to config, resolve from dars.config.json
808
+ file_arg = args.file
809
+ if file_arg in ('.', 'config', 'cfg'):
810
+ project_root = os.getcwd()
811
+ cfg, _found = load_config(project_root)
812
+ resolved = resolve_paths(cfg, project_root)
813
+ file_arg = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
814
+
815
+ # Validate entry file exists
816
+ if not os.path.exists(file_arg):
817
+ console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {file_arg}[/red]")
818
+ console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
819
+ sys.exit(1)
820
+
821
+ # Load application
822
+ app = exporter.load_app_from_file(file_arg)
823
+ if app is None:
824
+ sys.exit(1)
825
+
826
+ # Export
827
+ # If config exists and user didn't override output explicitly, use cfg.outdir
828
+ project_root = os.path.dirname(os.path.abspath(file_arg))
829
+ cfg, cfg_found = load_config(project_root)
830
+ outdir = args.output
831
+ if cfg_found and (args.output == './dist' or args.output == 'dist'):
832
+ resolved = resolve_paths(cfg, project_root)
833
+ outdir = resolved.get('outdir_abs') or outdir
834
+
835
+ # Validate format (currently only html)
836
+ if args.format not in ['html']:
837
+ console.print(f"[red]{translator.get('error_format_only_html')}[/red]")
838
+ sys.exit(1)
839
+
840
+ # Ensure outdir can be created
841
+ try:
842
+ os.makedirs(outdir, exist_ok=True)
843
+ except Exception as e:
844
+ console.print(f"[red]{translator.get('error_output_create')}: {outdir} -> {e}[/red]")
845
+ sys.exit(1)
846
+
847
+ ensure_dars_lib(project_root)
848
+ success = exporter.export_app(app, args.format, outdir, args.preview)
849
+ sys.exit(0 if success else 1)
850
+
851
+ elif args.command == 'info':
852
+ # Show information
853
+ app = exporter.load_app_from_file(args.file)
854
+ if app is None:
855
+ sys.exit(1)
856
+
857
+ exporter.show_app_info(app)
858
+
859
+ elif args.command == 'formats':
860
+ # Show formats
861
+ exporter.show_supported_formats()
862
+
863
+ elif args.command == 'init':
864
+ if args.list_templates:
865
+ list_templates_detailed()
866
+ elif args.update:
867
+ # Update or create config in provided name or current directory
868
+ target_dir = args.name or '.'
869
+ project_root = os.path.abspath(target_dir)
870
+ os.makedirs(project_root, exist_ok=True)
871
+ write_default_config(project_root, overwrite=False)
872
+ ensure_dars_lib(project_root)
873
+ console.print("[green]✔ dars.config.json created/updated[/green]")
874
+ elif not args.name:
875
+ console.print("[red]Error: Project name is required[/red]")
876
+ parser.parse_args(['init', '--help'])
877
+ else:
878
+ exporter.init_project(args.name, template=args.template)
879
+
880
+
881
+ elif args.command == 'build':
882
+ project_root = os.path.abspath(getattr(args, 'project', '.'))
883
+ cfg, found = load_config(project_root)
884
+ if not found:
885
+ console.print("[yellow][Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it.[/yellow]")
886
+ resolved = resolve_paths(cfg, project_root)
887
+ entry = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
888
+ format_name = cfg.get('format', 'html')
889
+ outdir = resolved.get('outdir_abs') or os.path.join(project_root, 'dist')
890
+
891
+ # Validate entry file exists
892
+ if not os.path.exists(entry):
893
+ console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {entry}[/red]")
894
+ console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
895
+ sys.exit(1)
896
+
897
+ # Validate format (currently only html)
898
+ if format_name not in ['html']:
899
+ console.print(f"[red]{translator.get('error_format_only_html')}[/red]")
900
+ sys.exit(1)
901
+
902
+ # Ensure outdir can be created
903
+ try:
904
+ os.makedirs(outdir, exist_ok=True)
905
+ except Exception as e:
906
+ console.print(f"[red]{translator.get('error_output_create')}: {outdir} -> {e}[/red]")
907
+ sys.exit(1)
908
+
909
+ ensure_dars_lib(project_root)
910
+ app = exporter.load_app_from_file(entry)
911
+ if app is None:
912
+ sys.exit(1)
913
+ success = exporter.export_app(app, format_name, outdir, show_preview=False)
914
+ sys.exit(0 if success else 1)
915
+
916
+ elif args.command == 'preview':
917
+ index_path = os.path.join(args.path, "index.html")
918
+ if os.path.exists(index_path):
919
+ console.print(f"[green]{translator.get('app_found')}: {args.path} [/green]")
920
+ console.print(f"{translator.get('open_in_browser')}: file://{os.path.abspath(index_path)}")
921
+ console.print(f"{translator.get('view_preview')} [green]y[/green] / [red]n[/red] [y/n] ")
922
+ if input().lower() == 'y':
923
+ # Pass the current language to preview.py
924
+
925
+ import subprocess
926
+ process = None
927
+ try:
928
+ process = subprocess.Popen([sys.executable, '-m', 'dars.cli.preview', args.path])
929
+ process.wait()
930
+ except KeyboardInterrupt:
931
+ if process:
932
+ process.terminate()
933
+ process.wait()
934
+ finally:
935
+ if process and process.poll() is None:
936
+ process.terminate()
937
+ process.wait()
938
+ else:
939
+ console.print(f"[red]{translator.get('index_not_found')} {args.path}[/red]")
940
+
941
+
942
+ elif args.command == 'config':
943
+ if getattr(args, 'config_command', None) == 'validate':
944
+ project_root = os.path.abspath(getattr(args, 'project', '.'))
945
+ cfg, found = load_config(project_root)
946
+ resolved = resolve_paths(cfg, project_root)
947
+
948
+ issues = []
949
+ def ok(msg):
950
+ return f"[green]✔ {msg}[/green]"
951
+ def warn(msg):
952
+ return f"[yellow]⚠ {msg}[/yellow]"
953
+ def err(msg):
954
+ return f"[red]✖ {msg}[/red]"
955
+
956
+ if not found:
957
+ issues.append(warn(translator.get('cfg_not_found_warn')))
958
+
959
+ # entry validation
960
+ entry = resolved.get('entry_abs')
961
+ if not entry or not os.path.isfile(entry):
962
+ issues.append(err(translator.get('cfg_entry_missing').format(path=cfg.get('entry'))))
963
+ else:
964
+ issues.append(ok(translator.get('cfg_entry_ok').format(path=cfg.get('entry'))))
965
+
966
+ # format validation
967
+ fmt = cfg.get('format')
968
+ if fmt != 'html':
969
+ issues.append(err(translator.get('cfg_format_only_html').format(fmt=fmt)))
970
+ else:
971
+ issues.append(ok(translator.get('cfg_format_ok').format(fmt=fmt)))
972
+
973
+ # outdir validation (creatable)
974
+ outdir_abs = resolved.get('outdir_abs')
975
+ try:
976
+ os.makedirs(outdir_abs, exist_ok=True)
977
+ issues.append(ok(translator.get('cfg_outdir_ok').format(path=cfg.get('outdir'))))
978
+ except Exception as e:
979
+ issues.append(err(translator.get('cfg_outdir_error').format(path=cfg.get('outdir'), error=str(e))))
980
+
981
+ # publicDir (if set) existence
982
+ pub = cfg.get('publicDir')
983
+ if pub:
984
+ pub_abs = resolved.get('public_abs')
985
+ if not pub_abs or not os.path.isdir(pub_abs):
986
+ issues.append(err(translator.get('cfg_public_missing').format(path=pub)))
987
+ else:
988
+ issues.append(ok(translator.get('cfg_public_ok').format(path=pub)))
989
+ else:
990
+ issues.append(warn(translator.get('cfg_public_autodetect')))
991
+
992
+ # include/exclude types
993
+ if not isinstance(cfg.get('include', []), list):
994
+ issues.append(err(translator.get('cfg_include_type')))
995
+ if not isinstance(cfg.get('exclude', []), list):
996
+ issues.append(err(translator.get('cfg_exclude_type')))
997
+
998
+ # bundle is bool
999
+ if not isinstance(cfg.get('bundle', False), bool):
1000
+ issues.append(err(translator.get('cfg_bundle_type')))
1001
+
1002
+ # Print report
1003
+ report = Table(title=translator.get('cfg_validation_title'))
1004
+ report.add_column(translator.get('cfg_item'), style="cyan")
1005
+ report.add_column(translator.get('cfg_result'), style="white")
1006
+
1007
+ report.add_row('config', translator.get('cfg_found') if found else translator.get('cfg_not_found'))
1008
+ for msg in issues:
1009
+ if 'entry' in msg:
1010
+ report.add_row('entry', msg)
1011
+ elif 'format' in msg:
1012
+ report.add_row('format', msg)
1013
+ elif 'outdir' in msg:
1014
+ report.add_row('outdir', msg)
1015
+ elif 'public' in msg or 'publicDir' in msg:
1016
+ report.add_row('publicDir', msg)
1017
+ elif 'include' in msg:
1018
+ report.add_row('include', msg)
1019
+ elif 'exclude' in msg:
1020
+ report.add_row('exclude', msg)
1021
+ elif 'bundle' in msg:
1022
+ report.add_row('bundle', msg)
1023
+ else:
1024
+ report.add_row('note', msg)
1025
+
1026
+ console.print(report)
1027
+ has_errors = any(msg.startswith('[red]') for msg in issues)
1028
+ sys.exit(1 if has_errors else 0)
1029
+ else:
1030
+ # Show help for config subcommands
1031
+ parser = create_parser(include_hidden=False)
1032
+ subparsers_actions = [action for action in parser._actions if isinstance(action, argparse._SubParsersAction)]
1033
+ for subparsers_action in subparsers_actions:
1034
+ if 'config' in subparsers_action.choices:
1035
+ pretty_print_help(subparsers_action.choices['config'])
1036
+ return
1037
+
1038
+ elif args.command == 'dev':
1039
+ # Resolve project and config
1040
+ project_root = os.path.abspath(getattr(args, 'project', '.'))
1041
+ cfg, found = load_config(project_root)
1042
+ if not found:
1043
+ console.print("[yellow][Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it.[/yellow]")
1044
+ resolved = resolve_paths(cfg, project_root)
1045
+ entry = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
1046
+
1047
+ if not os.path.exists(entry):
1048
+ console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {entry}[/red]")
1049
+ console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
1050
+ sys.exit(1)
1051
+
1052
+ # Ensure dars.min.js exists in project
1053
+ ensure_dars_lib(project_root)
1054
+ # Run entry in development mode (the entry typically calls app.rTimeCompile())
1055
+ import subprocess
1056
+ process = None
1057
+ try:
1058
+ console.print(f"[cyan]Running dev: {entry}[/cyan]")
1059
+ process = subprocess.Popen([sys.executable, entry], cwd=os.path.dirname(entry))
1060
+ process.wait()
1061
+ sys.exit(process.returncode or 0)
1062
+ except KeyboardInterrupt:
1063
+ if process:
1064
+ process.terminate()
1065
+ process.wait()
1066
+ sys.exit(0)
1067
+ except Exception as e:
1068
+ console.print(f"[red]Failed to start dev process: {e}[/red]")
1069
+ sys.exit(1)
1070
+
1071
+ elif args.command == 'doctor':
1072
+ # Run doctor with provided flags
1073
+ code = run_doctor(
1074
+ check_only=getattr(args, 'check', False),
1075
+ auto_yes=getattr(args, 'yes', False),
1076
+ install_all=getattr(args, 'all', False),
1077
+ force=getattr(args, 'force', False)
1078
+ )
1079
+ sys.exit(code)
1080
+
1081
+ elif args.command == 'forcedev':
1082
+ # Hidden: force-install Node, Bun, and all Python deps without prompts
1083
+ code = run_forcedev()
1084
+ sys.exit(code)
1085
+
1086
+ else:
1087
+ # Fallback: pretty help with header
1088
+ pretty_print_help(parser)
1089
+
1090
+ # Utility: ensure lib/dars.min.js exists at project root (no overwrite)
1091
+ def ensure_dars_lib(project_root: str):
1092
+ try:
1093
+ os.makedirs(os.path.join(project_root, 'lib'), exist_ok=True)
1094
+ dest = os.path.join(project_root, 'lib', 'dars.min.js')
1095
+ if not os.path.exists(dest):
1096
+ try:
1097
+ from dars.js_lib import DARS_MIN_JS
1098
+ with open(dest, 'w', encoding='utf-8') as fdst:
1099
+ fdst.write(DARS_MIN_JS)
1100
+ except Exception:
1101
+ pass
1102
+ except Exception:
1103
+ pass
1104
+
1105
+ if __name__ == "__main__":
1106
+ main()
1107
+