Coverage for fastblocks / cli.py: 31%
471 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:58 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:58 -0800
1import asyncio
2import logging
3import os
4import signal
5import typing as t
6from contextlib import suppress
7from enum import Enum
8from importlib.metadata import version as get_version
9from pathlib import Path
10from subprocess import DEVNULL
11from subprocess import run as execute
12from typing import Annotated
14with suppress(ImportError):
15 from acb.logger import InterceptHandler
17 for logger_name in ("uvicorn", "uvicorn.access", "uvicorn.error"):
18 logger = logging.getLogger(logger_name)
19 logger.handlers.clear()
20 logger.addHandler(InterceptHandler())
21 logger.setLevel(logging.DEBUG)
22 logger.propagate = False
24import sys
26import nest_asyncio
27import typer
28import uvicorn
29from acb.actions.encode import dump, load
30from anyio import Path as AsyncPath
31from granian import Granian
33try:
34 from acb.console import console
35except ImportError:
36 # Fallback to regular rich console if acb.console is not available
37 from rich.console import Console
39 console = Console()
41nest_asyncio.apply()
42__all__ = ("cli", "components", "create", "dev", "run")
43default_adapters = {
44 "routes": "default",
45 "templates": "jinja2",
46 "auth": "basic",
47 "sitemap": "asgi",
48}
49fastblocks_path = Path(__file__).parent
50apps_path = Path.cwd()
52# Check if running in the FastBlocks library directory itself
53# Skip this check during tests by examining if we're in a test environment
55is_testing = os.getenv("PYTEST_CURRENT_TEST") or "pytest" in sys.modules
56if Path.cwd() == fastblocks_path and not is_testing:
57 msg = "FastBlocks can not be run in the same directory as FastBlocks itself. Run `python -m fastblocks create`. Move into the app directory and try again."
58 raise SystemExit(
59 msg,
60 )
61cli = typer.Typer(rich_markup_mode="rich")
64class Styles(str, Enum):
65 bulma = "bulma"
66 webawesome = "webawesome"
67 custom = "custom"
69 def __str__(self) -> str:
70 return t.cast(str, self.value)
73run_args: dict[str, t.Any] = {"app": "main:app"}
74dev_args: dict[str, t.Any] = run_args | {"port": 8000, "reload": True}
75granian_dev_args: dict[str, t.Any] = dev_args | {
76 "address": "127.0.0.1",
77 "reload_paths": [Path.cwd(), fastblocks_path],
78 "interface": "asgi",
79 "log_enabled": False,
80 "log_access": True,
81 "reload_ignore_dirs": ["tmp", "settings", "templates"],
82}
83uvicorn_dev_args: dict[str, t.Any] = dev_args | {
84 "host": "127.0.0.1",
85 "reload_includes": ["*.py", str(Path.cwd()), str(fastblocks_path)],
86 "reload_excludes": ["tmp/*", "settings/*", "templates/*"],
87 "lifespan": "on",
88}
91def setup_signal_handlers() -> None:
92 import sys
94 def signal_handler(_signum: int, _frame: t.Any) -> None:
95 sys.exit(0)
97 signal.signal(signal.SIGINT, signal_handler)
98 signal.signal(signal.SIGTERM, signal_handler)
101@cli.command()
102def run(docker: bool = False, granian: bool = False, host: str = "127.0.0.1") -> None:
103 if docker:
104 execute(
105 f"docker run -it -ePORT=8080 -p8080:8080 {Path.cwd().stem}".split(),
106 )
107 else:
108 setup_signal_handlers()
109 if granian:
110 from granian.constants import Interfaces
112 Granian("main:app", address=host, interface=Interfaces.ASGI).serve()
113 else:
114 uvicorn.run(app=run_args["app"], host=host, lifespan="on", log_config=None)
117@cli.command()
118def dev(granian: bool = False) -> None:
119 setup_signal_handlers()
120 if granian:
121 from granian.constants import Interfaces
123 Granian(
124 "main:app",
125 address="127.0.0.1",
126 port=8000,
127 reload=True,
128 reload_paths=[Path.cwd(), fastblocks_path],
129 interface=Interfaces.ASGI,
130 log_enabled=False,
131 log_access=True,
132 ).serve()
133 else:
134 uvicorn.run(
135 app="main:app",
136 host="127.0.0.1",
137 port=8000,
138 reload=True,
139 reload_includes=["*.py", str(Path.cwd()), str(fastblocks_path)],
140 reload_excludes=["tmp/*", "settings/*", "templates/*"],
141 lifespan="on",
142 log_config=None,
143 )
146def _display_adapters() -> None:
147 from acb.adapters import get_adapters
149 console.print("[bold green]Available Adapters:[/bold green]")
150 adapters = get_adapters()
151 if not adapters:
152 console.print(" [dim]No adapters found[/dim]")
153 return
154 categories: dict[str, list[t.Any]] = {}
155 for adapter in adapters:
156 if adapter.category not in categories:
157 categories[adapter.category] = []
158 categories[adapter.category].append(adapter)
159 for category in sorted(categories.keys()):
160 console.print(f"\n [bold cyan]{category.upper()}:[/bold cyan]")
161 for adapter in sorted(categories[category], key=lambda a: a.name):
162 _display_adapter_info(adapter)
165def _display_adapter_info(adapter: t.Any) -> None:
166 status = "[green]✓[/green]" if adapter.installed else "[red]✗[/red]"
167 enabled = "[yellow]enabled[/yellow]" if adapter.enabled else "[dim]disabled[/dim]"
168 console.print(f" {status} [white]{adapter.name}[/white] - {enabled}")
169 if adapter.module:
170 console.print(f" [dim]{adapter.module}[/dim]")
173def _display_default_config() -> None:
174 console.print("\n[bold green]FastBlocks Default Configuration:[/bold green]")
175 for category, default_name in default_adapters.items():
176 console.print(f" [cyan]{category}[/cyan]: [white]{default_name}[/white]")
179def _display_actions() -> None:
180 console.print("\n[bold green]FastBlocks Actions:[/bold green]")
181 try:
182 from fastblocks.actions.minify import minify
184 console.print(" [cyan]minify[/cyan]:")
185 console.print(f" [white]- css[/white] ([dim]{minify.css.__name__}[/dim])")
186 console.print(f" [white]- js[/white] ([dim]{minify.js.__name__}[/dim])")
187 except ImportError:
188 console.print(" [dim]Minify actions not available[/dim]")
191@cli.command()
192def components() -> None:
193 try:
194 console.print("\n[bold blue]FastBlocks Components[/bold blue]\n")
195 _display_adapters()
196 _display_default_config()
197 _display_actions()
198 _display_htmy_commands()
199 except Exception as e:
200 console.print(f"[red]Error displaying components: {e}[/red]")
201 console.print("[dim]Make sure you're in a FastBlocks project directory[/dim]")
204def _display_htmy_commands() -> None:
205 console.print("\n[bold green]HTMY Component Commands:[/bold green]")
206 console.print(" [cyan]scaffold[/cyan]: Create new HTMY components")
207 console.print(" [cyan]list[/cyan]: List all discovered components")
208 console.print(" [cyan]validate[/cyan]: Validate component structure")
209 console.print(" [cyan]info[/cyan]: Get component metadata")
212# Component scaffolding helpers
213_COMPONENT_TYPE_MAP = {
214 "basic": "BASIC",
215 "dataclass": "DATACLASS",
216 "htmx": "HTMX",
217 "composite": "COMPOSITE",
218}
220_TYPE_MAP = {
221 "str": str,
222 "int": int,
223 "float": float,
224 "bool": bool,
225 "list": list,
226 "dict": dict,
227}
230def _parse_component_type(type_str: str) -> t.Any:
231 """Parse component type string to ComponentType enum."""
232 from fastblocks.adapters.templates._htmy_components import ComponentType
234 type_name = _COMPONENT_TYPE_MAP.get(type_str.lower(), "DATACLASS")
235 return getattr(ComponentType, type_name)
238def _parse_component_props(props: str) -> dict[str, type]:
239 """Parse props string into dict of name:type pairs."""
240 if not props:
241 return {}
243 parsed = {}
244 for prop_def in props.split(","):
245 if ":" not in prop_def:
246 continue
247 prop_name, prop_type = prop_def.strip().split(":", 1)
248 parsed[prop_name] = _TYPE_MAP.get(prop_type, str)
249 return parsed
252def _parse_component_children(children: str) -> list[str] | None:
253 """Parse children string into list of component names."""
254 if not children:
255 return None
256 return [c.strip() for c in children.split(",") if c.strip()]
259def _build_scaffold_kwargs(
260 parsed_props: dict[str, t.Any],
261 htmx: bool,
262 component_type: t.Any,
263 endpoint: str,
264 trigger: str,
265 target: str,
266 parsed_children: list[str] | None,
267) -> dict[str, t.Any]:
268 """Build kwargs dict for component scaffolding."""
269 from fastblocks.adapters.templates._htmy_components import ComponentType
271 kwargs: dict[str, t.Any] = {}
273 if parsed_props:
274 kwargs["props"] = parsed_props
276 if htmx or component_type == ComponentType.HTMX:
277 kwargs["htmx_enabled"] = True
278 if endpoint:
279 kwargs["endpoint"] = endpoint
280 kwargs["trigger"] = trigger
281 kwargs["target"] = target
283 if parsed_children:
284 kwargs["children"] = parsed_children
286 return kwargs
289@cli.command()
290def scaffold(
291 name: Annotated[str, typer.Argument(help="Component name")],
292 type: Annotated[
293 str,
294 typer.Option(
295 "--type", "-t", help="Component type: basic, dataclass, htmx, composite"
296 ),
297 ] = "dataclass",
298 htmx: Annotated[
299 bool, typer.Option("--htmx", "-x", help="Enable HTMX features")
300 ] = False,
301 endpoint: Annotated[
302 str, typer.Option("--endpoint", "-e", help="HTMX endpoint URL")
303 ] = "",
304 trigger: Annotated[
305 str, typer.Option("--trigger", help="HTMX trigger event")
306 ] = "click",
307 target: Annotated[
308 str, typer.Option("--target", help="HTMX target selector")
309 ] = "#content",
310 props: Annotated[
311 str,
312 typer.Option("--props", "-p", help="Component props as 'name:type,name:type'"),
313 ] = "",
314 children: Annotated[
315 str, typer.Option("--children", "-c", help="Child components as 'comp1,comp2'")
316 ] = "",
317 path: Annotated[str, typer.Option("--path", help="Custom component path")] = "",
318) -> None:
319 """Scaffold a new HTMY component."""
320 import asyncio
321 from pathlib import Path
323 async def scaffold_component() -> None:
324 try:
325 from acb.depends import depends
327 # Get HTMY adapter
328 htmy_adapter = await depends.get("htmy")
329 if htmy_adapter is None:
330 console.print(
331 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
332 )
333 return
335 # Parse inputs using helper functions
336 component_type = _parse_component_type(type)
337 parsed_props = _parse_component_props(props)
338 parsed_children = _parse_component_children(children)
340 # Build kwargs
341 kwargs = _build_scaffold_kwargs(
342 parsed_props,
343 htmx,
344 component_type,
345 endpoint,
346 trigger,
347 target,
348 parsed_children,
349 )
351 # Custom path
352 target_path = Path(path) if path else None
354 # Scaffold component
355 created_path = await htmy_adapter.scaffold_component(
356 name=name,
357 component_type=component_type,
358 target_path=target_path,
359 **kwargs,
360 )
362 console.print(
363 f"[green]✓[/green] Created {component_type.value} component '{name}' at {created_path}"
364 )
366 except Exception as e:
367 console.print(f"[red]Error scaffolding component: {e}[/red]")
369 asyncio.run(scaffold_component())
372def _get_component_status_color(status_value: str) -> str:
373 """Get the color for a component status."""
374 return {
375 "discovered": "yellow",
376 "validated": "green",
377 "compiled": "blue",
378 "ready": "green",
379 "error": "red",
380 "deprecated": "dim",
381 }.get(status_value, "white")
384def _display_component_entry(name: str, metadata: t.Any) -> None:
385 """Display a single component entry with status and metadata."""
386 status_color = _get_component_status_color(metadata.status.value)
388 console.print(
389 f" [{status_color}]●[/{status_color}] [white]{name}[/white] ({metadata.type.value})"
390 )
391 console.print(f" [dim]{metadata.path}[/dim]")
393 if metadata.error_message:
394 console.print(f" [red]Error: {metadata.error_message}[/red]")
395 elif metadata.docstring:
396 # Show first line of docstring
397 first_line = metadata.docstring.split("\n")[0].strip()
398 if first_line:
399 console.print(f" [dim]{first_line}[/dim]")
402@cli.command(name="list")
403def list_components() -> None:
404 """List all discovered HTMY components."""
405 import asyncio
407 async def list_all_components() -> None:
408 try:
409 from acb.depends import depends
411 htmy_adapter = await depends.get("htmy")
412 if htmy_adapter is None:
413 console.print(
414 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
415 )
416 return
418 components = await htmy_adapter.discover_components()
420 if not components:
421 console.print("[dim]No components found.[/dim]")
422 return
424 console.print(
425 f"\n[bold green]Found {len(components)} HTMY components:[/bold green]\n"
426 )
428 for name, metadata in components.items():
429 _display_component_entry(name, metadata)
431 except Exception as e:
432 console.print(f"[red]Error listing components: {e}[/red]")
434 asyncio.run(list_all_components())
437def _display_basic_metadata(component: str, metadata: t.Any) -> None:
438 """Display basic component metadata."""
439 console.print(f"\n[bold blue]Component: {component}[/bold blue]")
440 console.print(f" [cyan]Type:[/cyan] {metadata.type.value}")
441 console.print(f" [cyan]Status:[/cyan] {metadata.status.value}")
442 console.print(f" [cyan]Path:[/cyan] {metadata.path}")
445def _display_optional_metadata(metadata: t.Any) -> None:
446 """Display optional component metadata fields."""
447 if metadata.last_modified:
448 console.print(f" [cyan]Modified:[/cyan] {metadata.last_modified}")
450 if metadata.docstring:
451 console.print(f" [cyan]Description:[/cyan] {metadata.docstring}")
454def _display_htmx_attributes(metadata: t.Any) -> None:
455 """Display HTMX attributes if present."""
456 if metadata.htmx_attributes:
457 console.print(" [cyan]HTMX Attributes:[/cyan]")
458 for key, value in metadata.htmx_attributes.items():
459 console.print(f" {key}: {value}")
462def _display_dependencies(metadata: t.Any) -> None:
463 """Display component dependencies if present."""
464 if metadata.dependencies:
465 console.print(
466 f" [cyan]Dependencies:[/cyan] {', '.join(metadata.dependencies)}"
467 )
470def _display_status_message(metadata: t.Any) -> None:
471 """Display status-specific message."""
472 if metadata.status.value == "error":
473 console.print(f" [red]Error:[/red] {metadata.error_message}")
474 elif metadata.status.value == "ready":
475 console.print(" [green]✓ Component is ready[/green]")
478@cli.command()
479def validate(
480 component: Annotated[str, typer.Argument(help="Component name to validate")],
481) -> None:
482 """Validate a specific HTMY component."""
483 import asyncio
485 async def validate_component() -> None:
486 try:
487 from acb.depends import depends
489 htmy_adapter = await depends.get("htmy")
490 if htmy_adapter is None:
491 console.print(
492 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
493 )
494 return
496 metadata = await htmy_adapter.validate_component(component)
498 _display_basic_metadata(component, metadata)
499 _display_optional_metadata(metadata)
500 _display_htmx_attributes(metadata)
501 _display_dependencies(metadata)
502 _display_status_message(metadata)
504 except Exception as e:
505 console.print(f"[red]Error validating component '{component}': {e}[/red]")
507 asyncio.run(validate_component())
510@cli.command()
511def info(
512 component: Annotated[str, typer.Argument(help="Component name to get info for")],
513) -> None:
514 """Get detailed information about an HTMY component."""
515 import asyncio
517 def _display_component_class_info(
518 component_class: type, component_name: str
519 ) -> None:
520 """Display component class information including dataclass fields and HTMX status."""
521 console.print(f"\n[bold blue]Component: {component_name}[/bold blue]")
522 console.print(f" [cyan]Class:[/cyan] {component_class.__name__}")
523 console.print(f" [cyan]Module:[/cyan] {component_class.__module__}")
525 # Check if it's a dataclass
526 from dataclasses import fields, is_dataclass
528 if is_dataclass(component_class):
529 console.print(" [cyan]Fields:[/cyan]")
530 for field in fields(component_class):
531 console.print(f" {field.name}: {field.type}")
533 # Check for HTMX mixin
534 from fastblocks.adapters.templates._htmy_components import HTMXComponentMixin
536 if issubclass(component_class, HTMXComponentMixin):
537 console.print(" [cyan]HTMX Enabled:[/cyan] Yes")
539 def _display_component_metadata(metadata: t.Any) -> None:
540 """Display component metadata information."""
541 console.print(f" [cyan]Type:[/cyan] {metadata.type.value}")
542 console.print(f" [cyan]Status:[/cyan] {metadata.status.value}")
543 console.print(f" [cyan]Path:[/cyan] {metadata.path}")
545 async def get_component_info() -> None:
546 try:
547 from acb.depends import depends
549 htmy_adapter = await depends.get("htmy")
550 if htmy_adapter is None:
551 console.print(
552 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
553 )
554 return
556 # Get component metadata
557 metadata = await htmy_adapter.validate_component(component)
559 # Try to get component class for more info
560 try:
561 component_class = await htmy_adapter.get_component_class(component)
562 _display_component_class_info(component_class, component)
563 except Exception as e:
564 console.print(f"[red]Could not load component class: {e}[/red]")
566 # Show metadata
567 _display_component_metadata(metadata)
569 except Exception as e:
570 console.print(
571 f"[red]Error getting info for component '{component}': {e}[/red]"
572 )
574 asyncio.run(get_component_info())
577def _get_severity_color(severity: str) -> str:
578 """Get the color for an error severity level."""
579 return {
580 "error": "red",
581 "warning": "yellow",
582 "info": "blue",
583 "hint": "dim",
584 }.get(severity, "white")
587def _display_syntax_error(error: t.Any) -> None:
588 """Display a single syntax error with formatting."""
589 severity_color = _get_severity_color(error.severity)
591 console.print(
592 f" [{severity_color}]{error.severity.upper()}[/{severity_color}] "
593 f"Line {error.line + 1}, Column {error.column + 1}: {error.message}"
594 )
596 if error.fix_suggestion:
597 console.print(f" [dim]Fix: {error.fix_suggestion}[/dim]")
599 if error.code:
600 console.print(f" [dim]Code: {error.code}[/dim]")
603def _display_syntax_errors(file_path: str, errors: list[t.Any]) -> None:
604 """Display all syntax errors for a file."""
605 console.print(f"\n[bold red]Syntax errors found in {file_path}:[/bold red]")
606 for error in errors:
607 _display_syntax_error(error)
610@cli.command()
611def syntax_check(
612 file_path: Annotated[
613 str, typer.Argument(help="Path to FastBlocks template file to check")
614 ],
615 format_output: Annotated[
616 bool, typer.Option("--format", help="Format the output for better readability")
617 ] = False,
618) -> None:
619 """Check FastBlocks template syntax for errors and warnings."""
620 import asyncio
622 async def check_syntax() -> None:
623 try:
624 from pathlib import Path
626 from acb.depends import depends
628 syntax_support = await depends.get("syntax_support")
629 if syntax_support is None:
630 console.print(
631 "[red]Syntax support not available. Make sure you're in a FastBlocks project.[/red]"
632 )
633 return
635 template_path = Path(file_path)
636 if not template_path.exists():
637 console.print(f"[red]File not found: {file_path}[/red]")
638 return
640 content = template_path.read_text()
641 errors = syntax_support.check_syntax(content, template_path)
643 if not errors:
644 console.print(f"[green]✓ No syntax errors found in {file_path}[/green]")
645 return
647 _display_syntax_errors(file_path, errors)
649 except Exception as e:
650 console.print(f"[red]Error checking syntax: {e}[/red]")
652 asyncio.run(check_syntax())
655@cli.command()
656def format_template(
657 file_path: Annotated[
658 str, typer.Argument(help="Path to FastBlocks template file to format")
659 ],
660 in_place: Annotated[
661 bool, typer.Option("--in-place", "-i", help="Format file in place")
662 ] = False,
663) -> None:
664 """Format a FastBlocks template file."""
665 import asyncio
667 async def format_file() -> None:
668 try:
669 from pathlib import Path
671 from acb.depends import depends
673 syntax_support = await depends.get("syntax_support")
674 if syntax_support is None:
675 console.print(
676 "[red]Syntax support not available. Make sure you're in a FastBlocks project.[/red]"
677 )
678 return
680 template_path = Path(file_path)
681 if not template_path.exists():
682 console.print(f"[red]File not found: {file_path}[/red]")
683 return
685 content = template_path.read_text()
686 formatted = syntax_support.format_template(content)
688 if formatted == content:
689 console.print(f"[green]✓ File {file_path} is already formatted[/green]")
690 return
692 if in_place:
693 template_path.write_text(formatted)
694 console.print(f"[green]✓ Formatted {file_path} in place[/green]")
695 else:
696 console.print(formatted)
698 except Exception as e:
699 console.print(f"[red]Error formatting template: {e}[/red]")
701 asyncio.run(format_file())
704@cli.command()
705def generate_ide_config(
706 output_dir: Annotated[
707 str,
708 typer.Option("--output", "-o", help="Output directory for IDE configurations"),
709 ] = ".vscode",
710 ide: Annotated[
711 str, typer.Option(help="IDE to generate config for (vscode, vim, emacs)")
712 ] = "vscode",
713) -> None:
714 """Generate IDE configuration files for FastBlocks syntax support."""
715 import asyncio
716 import json
718 async def generate_config() -> None:
719 try:
720 from pathlib import Path
722 output_path = Path(output_dir)
723 output_path.mkdir(exist_ok=True)
725 if ide == "vscode":
726 # Generate VS Code extension configuration
727 from fastblocks.adapters.templates._language_server import (
728 generate_textmate_grammar,
729 generate_vscode_extension,
730 )
732 # Package.json for extension
733 package_json = generate_vscode_extension()
734 (output_path / "package.json").write_text(
735 json.dumps(package_json, indent=2)
736 )
738 # TextMate grammar
739 grammar = generate_textmate_grammar()
740 syntaxes_dir = output_path / "syntaxes"
741 syntaxes_dir.mkdir(exist_ok=True)
742 (syntaxes_dir / "fastblocks.tmLanguage.json").write_text(
743 json.dumps(grammar, indent=2)
744 )
746 # Language configuration
747 lang_config = {
748 "comments": {"blockComment": ["[#", "#]"]},
749 "brackets": [["[[", "]]"], ["[%", "%]"], ["[#", "#]"]],
750 "autoClosingPairs": [
751 {"open": "[[", "close": "]]"},
752 {"open": "[%", "close": "%]"},
753 {"open": "[#", "close": "#]"},
754 {"open": '"', "close": '"'},
755 {"open": "'", "close": "'"},
756 ],
757 "surroundingPairs": [
758 ["[[", "]]"],
759 ["[%", "%]"],
760 ["[#", "#]"],
761 ['"', '"'],
762 ["'", "'"],
763 ],
764 }
765 (output_path / "language-configuration.json").write_text(
766 json.dumps(lang_config, indent=2)
767 )
769 # Settings for FastBlocks language server
770 settings = {
771 "fastblocks.languageServer.enabled": True,
772 "fastblocks.languageServer.port": 7777,
773 "fastblocks.completion.enabled": True,
774 "fastblocks.diagnostics.enabled": True,
775 "files.associations": {
776 "*.fb.html": "fastblocks",
777 "*.fastblocks": "fastblocks",
778 },
779 }
780 (output_path / "settings.json").write_text(
781 json.dumps(settings, indent=2)
782 )
784 console.print(
785 f"[green]✓ VS Code configuration generated in {output_path}[/green]"
786 )
787 console.print(" Files created:")
788 console.print(" - package.json (extension manifest)")
789 console.print(" - language-configuration.json")
790 console.print(" - syntaxes/fastblocks.tmLanguage.json")
791 console.print(" - settings.json")
793 elif ide == "vim":
794 # Generate Vim configuration
795 vim_syntax = """
796" Vim syntax file for FastBlocks templates
797" Language: FastBlocks
798" Maintainer: FastBlocks Team
800if exists("b:current_syntax")
801 finish
802endif
804" FastBlocks delimiters
805syn region fastblocksVariable start="\\[\\[" end="\\]\\]" contains=fastblocksFilter,fastblocksString
806syn region fastblocksBlock start="\\[%" end="%\\]" contains=fastblocksKeyword,fastblocksString
807syn region fastblocksComment start="\\[#" end="#\\]"
809" Keywords
810syn keyword fastblocksKeyword if else elif endif for endfor block endblock extends include set macro endmacro
812" Filters
813syn match fastblocksFilter "|\\s*\\w\\+" contained
815" Strings
816syn region fastblocksString start='"' end='"' contained
817syn region fastblocksString start="'" end="'" contained
819" Highlighting
820hi def link fastblocksVariable Special
821hi def link fastblocksBlock Keyword
822hi def link fastblocksComment Comment
823hi def link fastblocksKeyword Statement
824hi def link fastblocksFilter Function
825hi def link fastblocksString String
827let b:current_syntax = "fastblocks"
828"""
829 (output_path / "fastblocks.vim").write_text(vim_syntax)
830 console.print(
831 f"[green]✓ Vim syntax file generated: {output_path}/fastblocks.vim[/green]"
832 )
834 elif ide == "emacs":
835 # Generate Emacs configuration
836 emacs_mode = """
837;;; fastblocks-mode.el --- Major mode for FastBlocks templates
839(defvar fastblocks-mode-syntax-table
840 (let ((table (make-syntax-table)))
841 (modify-syntax-entry ?\" "\\\"" table)
842 (modify-syntax-entry ?\' "\\\"" table)
843 table)
844 "Syntax table for `fastblocks-mode'.")
846(defvar fastblocks-font-lock-keywords
847 '(("\\\\[\\\\[.*?\\\\]\\\\]" . font-lock-variable-name-face)
848 ("\\\\[%.*?%\\\\]" . font-lock-keyword-face)
849 ("\\\\[#.*?#\\\\]" . font-lock-comment-face)
850 ("\\\\b\\\\(if\\\\|else\\\\|elif\\\\|endif\\\\|for\\\\|endfor\\\\|block\\\\|endblock\\\\|extends\\\\|include\\\\|set\\\\|macro\\\\|endmacro\\\\)\\\\b" . font-lock-builtin-face))
851 "Font lock keywords for FastBlocks mode.")
853(define-derived-mode fastblocks-mode html-mode "FastBlocks"
854 "Major mode for editing FastBlocks templates."
855 (setq font-lock-defaults '(fastblocks-font-lock-keywords)))
857(add-to-list 'auto-mode-alist '("\\\\.fb\\\\.html\\\\'" . fastblocks-mode))
858(add-to-list 'auto-mode-alist '("\\\\.fastblocks\\\\'" . fastblocks-mode))
860(provide 'fastblocks-mode)
861;;; fastblocks-mode.el ends here
862"""
863 (output_path / "fastblocks-mode.el").write_text(emacs_mode)
864 console.print(
865 f"[green]✓ Emacs mode file generated: {output_path}/fastblocks-mode.el[/green]"
866 )
868 else:
869 console.print(f"[red]Unsupported IDE: {ide}[/red]")
870 console.print("Supported IDEs: vscode, vim, emacs")
872 except Exception as e:
873 console.print(f"[red]Error generating IDE configuration: {e}[/red]")
875 asyncio.run(generate_config())
878@cli.command()
879def start_language_server(
880 port: Annotated[
881 int, typer.Option("--port", "-p", help="Port to run language server on")
882 ] = 7777,
883 host: Annotated[
884 str, typer.Option("--host", help="Host to bind language server to")
885 ] = "localhost",
886 stdio: Annotated[
887 bool, typer.Option("--stdio", help="Use stdio instead of TCP")
888 ] = False,
889) -> None:
890 """Start the FastBlocks Language Server."""
891 import asyncio
893 async def start_server() -> None:
894 try:
895 from acb.depends import depends
897 language_server = await depends.get("language_server")
898 if language_server is None:
899 console.print(
900 "[red]Language server not available. Make sure you're in a FastBlocks project.[/red]"
901 )
902 return
904 if stdio:
905 console.print(
906 "[blue]Starting FastBlocks Language Server in stdio mode...[/blue]"
907 )
908 # In a real implementation, this would handle stdio communication
909 console.print(
910 "[yellow]Stdio mode not yet implemented. Use TCP mode.[/yellow]"
911 )
912 else:
913 console.print(
914 f"[blue]Starting FastBlocks Language Server on {host}:{port}...[/blue]"
915 )
916 console.print("[green]Language Server started successfully![/green]")
917 console.print(f"Connect your IDE to: {host}:{port}")
919 # Keep server running using event-based approach
920 stop_event = asyncio.Event()
921 try:
922 await stop_event.wait()
923 except KeyboardInterrupt:
924 console.print("\n[yellow]Language server stopped.[/yellow]")
926 except Exception as e:
927 console.print(f"[red]Error starting language server: {e}[/red]")
929 asyncio.run(start_server())
932@cli.command()
933def create(
934 app_name: Annotated[
935 str,
936 typer.Option(prompt=True, help="Name of your application"),
937 ],
938 style: Annotated[
939 Styles,
940 typer.Option(
941 prompt=True,
942 help="The style (css, or web component, framework) you want to use[{','.join(Styles._member_names_)}]",
943 ),
944 ] = Styles.bulma,
945 domain: Annotated[
946 str,
947 typer.Option(prompt=True, help="Application domain"),
948 ] = "example.com",
949) -> None:
950 app_path = apps_path / app_name
951 app_path.mkdir(exist_ok=True)
952 os.chdir(app_path)
953 templates = Path("templates")
954 for p in (
955 templates / "base/blocks",
956 templates / f"{style}/blocks",
957 templates / f"{style}/theme",
958 Path("adapters"),
959 Path("actions"),
960 ):
961 p.mkdir(parents=True, exist_ok=True)
962 for p in (
963 Path("models.py"),
964 Path("routes.py"),
965 Path("main.py"),
966 Path(".envrc"),
967 Path("pyproject.toml"),
968 Path("__init__.py"),
969 Path("adapters/__init__.py"),
970 Path("actions/__init__.py"),
971 ):
972 p.touch()
973 for template_file in (
974 "main.py.tmpl",
975 ".envrc",
976 "pyproject.toml.tmpl",
977 "Procfile.tmpl",
978 ):
979 template_path = Path(template_file)
980 target_path = Path(template_file.replace(".tmpl", ""))
981 target_path.write_text(
982 (fastblocks_path / template_path).read_text().replace("APP_NAME", app_name),
983 )
984 commands = (
985 ["direnv", "allow", "."],
986 ["pdm", "install"],
987 ["python", "-m", "fastblocks", "run"],
988 )
989 for command in commands:
990 execute(command, stdout=DEVNULL, stderr=DEVNULL)
992 async def update_settings(settings: str, values: dict[str, t.Any]) -> None:
993 settings_path = AsyncPath(app_path / "settings")
994 settings_dict = await load.yaml(settings_path / f"{settings}.yml")
995 settings_dict.update(values)
996 await dump.yaml(settings_dict, settings_path / f"{settings}.yml")
998 async def update_configs() -> None:
999 await update_settings("debug", {"fastblocks": False})
1000 await update_settings("adapters", default_adapters)
1001 await update_settings(
1002 "app", {"title": "Welcome to FastBlocks", "domain": domain}
1003 )
1005 asyncio.run(update_configs())
1006 console.print(
1007 f"\n[bold][white]Project is initialized. Please configure [green]'adapters.yml'[/] and [green]'app.yml'[/] in the [blue]'{app_name}/settings'[/] directory before running [magenta]`python -m fastblocks dev`[/] or [magenta]`python -m fastblocks run`[/] from the [blue]'{app_name}'[/] directory.[/][/]",
1008 )
1009 console.print(
1010 "\n[dim]Use [white]`python -m fastblocks components`[/white] to see available adapters and actions.[/dim]",
1011 )
1012 raise SystemExit
1015@cli.command()
1016def version() -> None:
1017 try:
1018 __version__ = get_version("fastblocks")
1019 console.print(f"FastBlocks v{__version__}")
1020 except Exception:
1021 console.print("Unable to determine FastBlocks version")
1024@cli.command()
1025def mcp(
1026 port: Annotated[
1027 int,
1028 typer.Option("--port", "-p", help="Port for MCP server (default: auto)"),
1029 ] = 0,
1030 host: Annotated[
1031 str, typer.Option("--host", help="Host to bind MCP server to")
1032 ] = "localhost",
1033) -> None:
1034 """Start the FastBlocks MCP (Model Context Protocol) server.
1036 Enables IDE/AI assistant integration for FastBlocks development
1037 including template management, component creation, and adapter configuration.
1038 """
1039 import asyncio
1041 async def start_mcp_server() -> None:
1042 try:
1043 console.print("\n[bold blue]FastBlocks MCP Server[/bold blue]")
1044 console.print("[dim]Model Context Protocol for IDE Integration[/dim]\n")
1046 from .mcp import create_fastblocks_mcp_server
1048 console.print("[yellow]Initializing MCP server...[/yellow]")
1049 server = await create_fastblocks_mcp_server()
1051 console.print("[green]✓ MCP server initialized[/green]")
1053 if port:
1054 console.print(f"[blue]Starting server on {host}:{port}...[/blue]")
1055 else:
1056 console.print("[blue]Starting server...[/blue]")
1058 console.print("[green]✓ FastBlocks MCP server running[/green]")
1059 console.print("\n[dim]Press Ctrl+C to stop[/dim]\n")
1061 await server.start()
1063 except KeyboardInterrupt:
1064 console.print("\n[yellow]MCP server stopped by user[/yellow]")
1065 except ImportError as e:
1066 console.print(f"[red]MCP dependencies not available: {e}[/red]")
1067 console.print(
1068 "[dim]Make sure ACB is installed with MCP support (acb>=0.23.0)[/dim]"
1069 )
1070 except Exception as e:
1071 console.print(f"[red]Error starting MCP server: {e}[/red]")
1073 asyncio.run(start_mcp_server())