base_commands.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. """Base commands for module: list, search, show, validate, generate."""
  2. from __future__ import annotations
  3. import logging
  4. import os
  5. from pathlib import Path
  6. from jinja2 import Template as Jinja2Template
  7. from typer import Exit
  8. from ..config import ConfigManager
  9. from ..display import DisplayManager, IconManager
  10. from ..exceptions import (
  11. TemplateRenderError,
  12. TemplateSyntaxError,
  13. TemplateValidationError,
  14. )
  15. from ..input import InputManager
  16. from ..template import Template
  17. from ..validators import get_validator_registry
  18. from .helpers import (
  19. apply_cli_overrides,
  20. apply_var_file,
  21. apply_variable_defaults,
  22. collect_variable_values,
  23. )
  24. logger = logging.getLogger(__name__)
  25. # File size thresholds for display formatting
  26. BYTES_PER_KB = 1024
  27. BYTES_PER_MB = 1024 * 1024
  28. def list_templates(module_instance, raw: bool = False) -> list:
  29. """List all templates."""
  30. logger.debug(f"Listing templates for module '{module_instance.name}'")
  31. # Load all templates using centralized helper
  32. filtered_templates = module_instance._load_all_templates()
  33. if filtered_templates:
  34. if raw:
  35. # Output raw format (tab-separated values for easy filtering with awk/sed/cut)
  36. # Format: ID\tNAME\tTAGS\tVERSION\tLIBRARY
  37. for template in filtered_templates:
  38. name = template.metadata.name or "Unnamed Template"
  39. tags_list = template.metadata.tags or []
  40. tags = ",".join(tags_list) if tags_list else "-"
  41. version = (
  42. str(template.metadata.version) if template.metadata.version else "-"
  43. )
  44. library = template.metadata.library or "-"
  45. print(f"{template.id}\t{name}\t{tags}\t{version}\t{library}")
  46. else:
  47. # Output rich table format
  48. def format_template_row(template):
  49. name = template.metadata.name or "Unnamed Template"
  50. tags_list = template.metadata.tags or []
  51. tags = ", ".join(tags_list) if tags_list else "-"
  52. version = str(template.metadata.version) if template.metadata.version else ""
  53. schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
  54. library_name = template.metadata.library or ""
  55. library_type = template.metadata.library_type or "git"
  56. # Format library with icon and color
  57. icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
  58. color = "yellow" if library_type == "static" else "blue"
  59. library_display = f"[{color}]{icon} {library_name}[/{color}]"
  60. return (template.id, name, tags, version, schema, library_display)
  61. module_instance.display.data_table(
  62. columns=[
  63. {"name": "ID", "style": "bold", "no_wrap": True},
  64. {"name": "Name"},
  65. {"name": "Tags"},
  66. {"name": "Version", "no_wrap": True},
  67. {"name": "Schema", "no_wrap": True},
  68. {"name": "Library", "no_wrap": True},
  69. ],
  70. rows=filtered_templates,
  71. title=f"{module_instance.name.capitalize()} templates",
  72. row_formatter=format_template_row,
  73. )
  74. else:
  75. logger.info(f"No templates found for module '{module_instance.name}'")
  76. return filtered_templates
  77. def search_templates(module_instance, query: str) -> list:
  78. """Search for templates by ID containing the search string."""
  79. logger.debug(
  80. f"Searching templates for module '{module_instance.name}' with query='{query}'"
  81. )
  82. # Load templates with search filter using centralized helper
  83. filtered_templates = module_instance._load_all_templates(
  84. lambda t: query.lower() in t.id.lower()
  85. )
  86. if filtered_templates:
  87. logger.info(
  88. f"Found {len(filtered_templates)} templates matching '{query}' for module '{module_instance.name}'"
  89. )
  90. def format_template_row(template):
  91. name = template.metadata.name or "Unnamed Template"
  92. tags_list = template.metadata.tags or []
  93. tags = ", ".join(tags_list) if tags_list else "-"
  94. version = str(template.metadata.version) if template.metadata.version else ""
  95. schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
  96. library_name = template.metadata.library or ""
  97. library_type = template.metadata.library_type or "git"
  98. # Format library with icon and color
  99. icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
  100. color = "yellow" if library_type == "static" else "blue"
  101. library_display = f"[{color}]{icon} {library_name}[/{color}]"
  102. return (template.id, name, tags, version, schema, library_display)
  103. module_instance.display.data_table(
  104. columns=[
  105. {"name": "ID", "style": "bold", "no_wrap": True},
  106. {"name": "Name"},
  107. {"name": "Tags"},
  108. {"name": "Version", "no_wrap": True},
  109. {"name": "Schema", "no_wrap": True},
  110. {"name": "Library", "no_wrap": True},
  111. ],
  112. rows=filtered_templates,
  113. title=f"{module_instance.name.capitalize()} templates matching '{query}'",
  114. row_formatter=format_template_row,
  115. )
  116. else:
  117. logger.info(
  118. f"No templates found matching '{query}' for module '{module_instance.name}'"
  119. )
  120. module_instance.display.warning(
  121. f"No templates found matching '{query}'",
  122. context=f"module '{module_instance.name}'",
  123. )
  124. return filtered_templates
  125. def show_template(
  126. module_instance, id: str, var: list[str] | None = None, var_file: str | None = None
  127. ) -> None:
  128. """Show template details with optional variable overrides."""
  129. logger.debug(f"Showing template '{id}' from module '{module_instance.name}'")
  130. template = module_instance._load_template_by_id(id)
  131. if not template:
  132. module_instance.display.error(
  133. f"Template '{id}' not found", context=f"module '{module_instance.name}'"
  134. )
  135. return
  136. # Apply defaults and overrides (same precedence as generate command)
  137. if template.variables:
  138. config = ConfigManager()
  139. apply_variable_defaults(template, config, module_instance.name)
  140. apply_var_file(template, var_file, module_instance.display)
  141. apply_cli_overrides(template, var)
  142. # Re-sort sections after applying overrides (toggle values may have changed)
  143. template.variables.sort_sections()
  144. # Reset disabled bool variables to False to prevent confusion
  145. reset_vars = template.variables.reset_disabled_bool_variables()
  146. if reset_vars:
  147. logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
  148. # Display template header
  149. module_instance.display.templates.render_template_header(template, id)
  150. # Display file tree
  151. module_instance.display.templates.render_file_tree(template)
  152. # Display variables table
  153. module_instance.display.variables.render_variables_table(template)
  154. def check_output_directory(
  155. output_dir: Path,
  156. rendered_files: dict[str, str],
  157. interactive: bool,
  158. display: DisplayManager,
  159. ) -> list[Path] | None:
  160. """Check output directory for conflicts and get user confirmation if needed."""
  161. dir_exists = output_dir.exists()
  162. dir_not_empty = dir_exists and any(output_dir.iterdir())
  163. # Check which files already exist
  164. existing_files = []
  165. if dir_exists:
  166. for file_path in rendered_files:
  167. full_path = output_dir / file_path
  168. if full_path.exists():
  169. existing_files.append(full_path)
  170. # Warn if directory is not empty
  171. if dir_not_empty:
  172. if interactive:
  173. display.warning(f"Directory '{output_dir}' is not empty.")
  174. if existing_files:
  175. display.text(f" {len(existing_files)} file(s) will be overwritten.")
  176. input_mgr = InputManager()
  177. if not input_mgr.confirm("Continue?", default=False):
  178. display.info("Generation cancelled")
  179. return None
  180. else:
  181. # Non-interactive mode: show warning but continue
  182. logger.warning(f"Directory '{output_dir}' is not empty")
  183. if existing_files:
  184. logger.warning(f"{len(existing_files)} file(s) will be overwritten")
  185. return existing_files
  186. def get_generation_confirmation(
  187. output_dir: Path,
  188. rendered_files: dict[str, str],
  189. existing_files: list[Path] | None,
  190. dir_not_empty: bool,
  191. dry_run: bool,
  192. interactive: bool,
  193. display: DisplayManager,
  194. ) -> bool:
  195. """Display file generation confirmation and get user approval."""
  196. if not interactive:
  197. return True
  198. # Use templates.render_file_generation_confirmation directly for now
  199. display.templates.render_file_generation_confirmation(
  200. output_dir, rendered_files, existing_files if existing_files else None
  201. )
  202. # Final confirmation (only if we didn't already ask about overwriting)
  203. if not dir_not_empty and not dry_run:
  204. input_mgr = InputManager()
  205. if not input_mgr.confirm("Generate these files?", default=True):
  206. display.info("Generation cancelled")
  207. return False
  208. return True
  209. def _check_directory_permissions(output_dir: Path, display: DisplayManager) -> None:
  210. """Check directory existence and write permissions."""
  211. if output_dir.exists():
  212. display.success(f"Output directory exists: [cyan]{output_dir}[/cyan]")
  213. if os.access(output_dir, os.W_OK):
  214. display.success("Write permission verified")
  215. else:
  216. display.warning("Write permission may be denied")
  217. else:
  218. display.info(
  219. f" [dim]→[/dim] Would create output directory: [cyan]{output_dir}[/cyan]"
  220. )
  221. parent = output_dir.parent
  222. if parent.exists() and os.access(parent, os.W_OK):
  223. display.success("Parent directory writable")
  224. else:
  225. display.warning("Parent directory may not be writable")
  226. def _collect_subdirectories(rendered_files: dict[str, str]) -> set[Path]:
  227. """Collect unique subdirectories from file paths."""
  228. subdirs = set()
  229. for file_path in rendered_files:
  230. parts = Path(file_path).parts
  231. for i in range(1, len(parts)):
  232. subdirs.add(Path(*parts[:i]))
  233. return subdirs
  234. def _analyze_file_operations(
  235. output_dir: Path, rendered_files: dict[str, str]
  236. ) -> tuple[list[tuple[str, int, str]], int, int, int]:
  237. """Analyze file operations and return statistics."""
  238. total_size = 0
  239. new_files = 0
  240. overwrite_files = 0
  241. file_operations = []
  242. for file_path, content in sorted(rendered_files.items()):
  243. full_path = output_dir / file_path
  244. file_size = len(content.encode("utf-8"))
  245. total_size += file_size
  246. if full_path.exists():
  247. status = "Overwrite"
  248. overwrite_files += 1
  249. else:
  250. status = "Create"
  251. new_files += 1
  252. file_operations.append((file_path, file_size, status))
  253. return file_operations, total_size, new_files, overwrite_files
  254. def _format_size(total_size: int) -> str:
  255. """Format byte size into human-readable string."""
  256. if total_size < BYTES_PER_KB:
  257. return f"{total_size}B"
  258. elif total_size < BYTES_PER_MB:
  259. return f"{total_size / BYTES_PER_KB:.1f}KB"
  260. else:
  261. return f"{total_size / BYTES_PER_MB:.1f}MB"
  262. def execute_dry_run(
  263. id: str,
  264. output_dir: Path,
  265. rendered_files: dict[str, str],
  266. show_files: bool,
  267. display: DisplayManager,
  268. ) -> None:
  269. """Execute dry run mode with comprehensive simulation."""
  270. display.info("")
  271. display.info("[bold cyan]Dry Run Mode - Simulating File Generation[/bold cyan]")
  272. display.info("")
  273. # Simulate directory creation
  274. display.heading("Directory Operations")
  275. _check_directory_permissions(output_dir, display)
  276. # Collect and display subdirectories
  277. subdirs = _collect_subdirectories(rendered_files)
  278. if subdirs:
  279. display.info(f" [dim]→[/dim] Would create {len(subdirs)} subdirectory(ies)")
  280. for subdir in sorted(subdirs):
  281. display.info(f" [dim]📁[/dim] {subdir}/")
  282. display.info("")
  283. # Display file operations in a table
  284. display.heading("File Operations")
  285. file_operations, total_size, new_files, overwrite_files = _analyze_file_operations(
  286. output_dir, rendered_files
  287. )
  288. # Use data_table for file operations
  289. display.data_table(
  290. columns=[
  291. {"name": "File", "no_wrap": False},
  292. {"name": "Size", "justify": "right", "style": "dim"},
  293. {"name": "Status", "style": "yellow"},
  294. ],
  295. rows=file_operations,
  296. row_formatter=lambda row: (str(row[0]), display.format_file_size(row[1]), row[2]),
  297. )
  298. display.info("")
  299. # Summary statistics
  300. size_str = _format_size(total_size)
  301. summary_rows = [
  302. ("Total files:", str(len(rendered_files))),
  303. ("New files:", str(new_files)),
  304. ("Files to overwrite:", str(overwrite_files)),
  305. ("Total size:", size_str),
  306. ]
  307. display.table(headers=None, rows=summary_rows, title="Summary", show_header=False, borderless=True)
  308. display.info("")
  309. # Show file contents if requested
  310. if show_files:
  311. display.info("[bold cyan]Generated File Contents:[/bold cyan]")
  312. display.info("")
  313. for file_path, content in sorted(rendered_files.items()):
  314. display.info(f"[cyan]File:[/cyan] {file_path}")
  315. display.info(f"{'─' * 80}")
  316. display.info(content)
  317. display.info("") # Add blank line after content
  318. display.info("")
  319. display.success("Dry run complete - no files were written")
  320. display.info(f"[dim]Files would have been generated in '{output_dir}'[/dim]")
  321. logger.info(
  322. f"Dry run completed for template '{id}' - {len(rendered_files)} files, {total_size} bytes"
  323. )
  324. def write_generated_files(
  325. output_dir: Path,
  326. rendered_files: dict[str, str],
  327. quiet: bool,
  328. display: DisplayManager,
  329. ) -> None:
  330. """Write rendered files to the output directory."""
  331. output_dir.mkdir(parents=True, exist_ok=True)
  332. for file_path, content in rendered_files.items():
  333. full_path = output_dir / file_path
  334. full_path.parent.mkdir(parents=True, exist_ok=True)
  335. with open(full_path, "w", encoding="utf-8") as f:
  336. f.write(content)
  337. if not quiet:
  338. display.success(f"Generated file: {file_path}")
  339. if not quiet:
  340. display.success(f"Template generated successfully in '{output_dir}'")
  341. logger.info(f"Template written to directory: {output_dir}")
  342. def _prepare_template(
  343. module_instance,
  344. id: str,
  345. var_file: str | None,
  346. var: list[str] | None,
  347. display: DisplayManager,
  348. ):
  349. """Load template and apply all defaults/overrides."""
  350. template = module_instance._load_template_by_id(id)
  351. config = ConfigManager()
  352. apply_variable_defaults(template, config, module_instance.name)
  353. apply_var_file(template, var_file, display)
  354. apply_cli_overrides(template, var)
  355. if template.variables:
  356. template.variables.sort_sections()
  357. reset_vars = template.variables.reset_disabled_bool_variables()
  358. if reset_vars:
  359. logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
  360. return template
  361. def _render_template(template, id: str, display: DisplayManager, interactive: bool):
  362. """Validate, render template and collect variable values."""
  363. variable_values = collect_variable_values(template, interactive)
  364. if template.variables:
  365. template.variables.validate_all()
  366. debug_mode = logger.isEnabledFor(logging.DEBUG)
  367. rendered_files, variable_values = template.render(
  368. template.variables, debug=debug_mode
  369. )
  370. if not rendered_files:
  371. display.error(
  372. "Template rendering returned no files",
  373. context="template generation",
  374. )
  375. raise Exit(code=1)
  376. logger.info(f"Successfully rendered template '{id}'")
  377. return rendered_files, variable_values
  378. def _determine_output_dir(directory: str | None, id: str) -> Path:
  379. """Determine and normalize output directory path."""
  380. if directory:
  381. output_dir = Path(directory)
  382. if not output_dir.is_absolute() and str(output_dir).startswith(
  383. ("Users/", "home/", "usr/", "opt/", "var/", "tmp/")
  384. ):
  385. output_dir = Path("/") / output_dir
  386. logger.debug(f"Normalized relative-looking absolute path to: {output_dir}")
  387. else:
  388. output_dir = Path(id)
  389. return output_dir
  390. def generate_template(
  391. module_instance,
  392. id: str,
  393. directory: str | None,
  394. interactive: bool,
  395. var: list[str] | None,
  396. var_file: str | None,
  397. dry_run: bool,
  398. show_files: bool,
  399. quiet: bool,
  400. ) -> None:
  401. """Generate from template."""
  402. logger.info(
  403. f"Starting generation for template '{id}' from module '{module_instance.name}'"
  404. )
  405. display = DisplayManager(quiet=quiet) if quiet else module_instance.display
  406. template = _prepare_template(module_instance, id, var_file, var, display)
  407. if not quiet:
  408. # Display template header
  409. module_instance.display.templates.render_template_header(template, id)
  410. # Display file tree
  411. module_instance.display.templates.render_file_tree(template)
  412. # Display variables table
  413. module_instance.display.variables.render_variables_table(template)
  414. module_instance.display.info("")
  415. try:
  416. rendered_files, variable_values = _render_template(
  417. template, id, display, interactive
  418. )
  419. output_dir = _determine_output_dir(directory, id)
  420. # Check for conflicts and get confirmation (skip in quiet mode)
  421. if not quiet:
  422. existing_files = check_output_directory(
  423. output_dir, rendered_files, interactive, display
  424. )
  425. if existing_files is None:
  426. return # User cancelled
  427. dir_not_empty = output_dir.exists() and any(output_dir.iterdir())
  428. if not get_generation_confirmation(
  429. output_dir,
  430. rendered_files,
  431. existing_files,
  432. dir_not_empty,
  433. dry_run,
  434. interactive,
  435. display,
  436. ):
  437. return # User cancelled
  438. # Execute generation (dry run or actual)
  439. if dry_run:
  440. if not quiet:
  441. execute_dry_run(id, output_dir, rendered_files, show_files, display)
  442. else:
  443. write_generated_files(output_dir, rendered_files, quiet, display)
  444. # Display next steps (not in quiet mode)
  445. if template.metadata.next_steps and not quiet:
  446. display.heading("Next Steps")
  447. try:
  448. next_steps_template = Jinja2Template(template.metadata.next_steps)
  449. rendered_next_steps = next_steps_template.render(variable_values)
  450. display.text(rendered_next_steps)
  451. except Exception as e:
  452. logger.warning(f"Failed to render next_steps as template: {e}")
  453. # Fallback to plain text if rendering fails
  454. display.text(template.metadata.next_steps)
  455. except TemplateRenderError as e:
  456. display.error(str(e), context=f"template '{id}'")
  457. raise Exit(code=1) from None
  458. except Exception as e:
  459. display.error(str(e), context=f"generating template '{id}'")
  460. raise Exit(code=1) from None
  461. def validate_templates(
  462. module_instance,
  463. template_id: str,
  464. path: str | None,
  465. verbose: bool,
  466. semantic: bool,
  467. ) -> None:
  468. """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
  469. # Load template based on input
  470. template = _load_template_for_validation(module_instance, template_id, path)
  471. if template:
  472. _validate_single_template(
  473. module_instance, template, template_id, verbose, semantic
  474. )
  475. else:
  476. _validate_all_templates(module_instance, verbose)
  477. def _load_template_for_validation(module_instance, template_id: str, path: str | None):
  478. """Load a template from path or ID for validation."""
  479. if path:
  480. template_path = Path(path).resolve()
  481. if not template_path.exists():
  482. module_instance.display.error(f"Path does not exist: {path}")
  483. raise Exit(code=1) from None
  484. if not template_path.is_dir():
  485. module_instance.display.error(f"Path is not a directory: {path}")
  486. raise Exit(code=1) from None
  487. module_instance.display.info(
  488. f"[bold]Validating template from path:[/bold] [cyan]{template_path}[/cyan]"
  489. )
  490. try:
  491. return Template(template_path, library_name="local")
  492. except Exception as e:
  493. module_instance.display.error(
  494. f"Failed to load template from path '{path}': {e}"
  495. )
  496. raise Exit(code=1) from None
  497. if template_id:
  498. try:
  499. template = module_instance._load_template_by_id(template_id)
  500. module_instance.display.info(
  501. f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]"
  502. )
  503. return template
  504. except Exception as e:
  505. module_instance.display.error(
  506. f"Failed to load template '{template_id}': {e}"
  507. )
  508. raise Exit(code=1) from None
  509. return None
  510. def _validate_single_template(
  511. module_instance, template, template_id: str, verbose: bool, semantic: bool
  512. ) -> None:
  513. """Validate a single template."""
  514. try:
  515. # Jinja2 validation
  516. _ = template.used_variables
  517. _ = template.variables
  518. module_instance.display.success("Jinja2 validation passed")
  519. # Semantic validation
  520. if semantic:
  521. _run_semantic_validation(module_instance, template, verbose)
  522. # Verbose output
  523. if verbose:
  524. _display_validation_details(module_instance, template, semantic)
  525. except TemplateRenderError as e:
  526. module_instance.display.error(str(e), context=f"template '{template_id}'")
  527. raise Exit(code=1) from None
  528. except (TemplateSyntaxError, TemplateValidationError, ValueError) as e:
  529. module_instance.display.error(f"Validation failed for '{template_id}':")
  530. module_instance.display.info(f"\n{e}")
  531. raise Exit(code=1) from None
  532. except Exception as e:
  533. module_instance.display.error(
  534. f"Unexpected error validating '{template_id}': {e}"
  535. )
  536. raise Exit(code=1) from None
  537. def _run_semantic_validation(module_instance, template, verbose: bool) -> None:
  538. """Run semantic validation on rendered template files."""
  539. module_instance.display.info("")
  540. module_instance.display.info(
  541. "[bold cyan]Running semantic validation...[/bold cyan]"
  542. )
  543. registry = get_validator_registry()
  544. debug_mode = logger.isEnabledFor(logging.DEBUG)
  545. rendered_files, _ = template.render(template.variables, debug=debug_mode)
  546. has_semantic_errors = False
  547. for file_path, content in rendered_files.items():
  548. result = registry.validate_file(content, file_path)
  549. if result.errors or result.warnings or (verbose and result.info):
  550. module_instance.display.info(f"\n[cyan]File:[/cyan] {file_path}")
  551. result.display(f"{file_path}")
  552. if result.errors:
  553. has_semantic_errors = True
  554. if has_semantic_errors:
  555. module_instance.display.error("Semantic validation found errors")
  556. raise Exit(code=1) from None
  557. module_instance.display.success("Semantic validation passed")
  558. def _display_validation_details(module_instance, template, semantic: bool) -> None:
  559. """Display verbose validation details."""
  560. module_instance.display.info(f"\n[dim]Template path: {template.template_dir}[/dim]")
  561. module_instance.display.info(
  562. f"[dim]Found {len(template.used_variables)} variables[/dim]"
  563. )
  564. if semantic:
  565. debug_mode = logger.isEnabledFor(logging.DEBUG)
  566. rendered_files, _ = template.render(template.variables, debug=debug_mode)
  567. module_instance.display.info(
  568. f"[dim]Generated {len(rendered_files)} files[/dim]"
  569. )
  570. def _validate_all_templates(module_instance, verbose: bool) -> None:
  571. """Validate all templates in the module."""
  572. module_instance.display.info(
  573. f"[bold]Validating all {module_instance.name} templates...[/bold]"
  574. )
  575. valid_count = 0
  576. invalid_count = 0
  577. errors = []
  578. all_templates = module_instance._load_all_templates()
  579. total = len(all_templates)
  580. for template in all_templates:
  581. try:
  582. _ = template.used_variables
  583. _ = template.variables
  584. valid_count += 1
  585. if verbose:
  586. module_instance.display.success(template.id)
  587. except ValueError as e:
  588. invalid_count += 1
  589. errors.append((template.id, str(e)))
  590. if verbose:
  591. module_instance.display.error(template.id)
  592. except Exception as e:
  593. invalid_count += 1
  594. errors.append((template.id, f"Load error: {e}"))
  595. if verbose:
  596. module_instance.display.warning(template.id)
  597. # Display summary
  598. summary_rows = [
  599. ("Total templates:", str(total)),
  600. ("[green]Valid:[/green]", str(valid_count)),
  601. ("[red]Invalid:[/red]", str(invalid_count)),
  602. ]
  603. module_instance.display.table(headers=None, rows=summary_rows, title="Validation Summary", show_header=False, borderless=True)
  604. if errors:
  605. module_instance.display.info("")
  606. module_instance.display.error("Validation Errors:")
  607. for template_id, error_msg in errors:
  608. module_instance.display.info(
  609. f"\n[yellow]Template:[/yellow] [cyan]{template_id}[/cyan]"
  610. )
  611. module_instance.display.info(f"[dim]{error_msg}[/dim]")
  612. raise Exit(code=1)
  613. module_instance.display.success("All templates are valid!")