Coverage for fastblocks/cli.py: 0%
468 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
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 nest_asyncio
25import typer
26import uvicorn
27from acb.actions.encode import dump, load
28from acb.console import console
29from anyio import Path as AsyncPath
30from granian import Granian
32nest_asyncio.apply()
33__all__ = ("cli", "components", "create", "dev", "run")
34default_adapters = {
35 "routes": "default",
36 "templates": "jinja2",
37 "auth": "basic",
38 "sitemap": "asgi",
39}
40fastblocks_path = Path(__file__).parent
41apps_path = Path.cwd()
42if Path.cwd() == fastblocks_path:
43 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."
44 raise SystemExit(
45 msg,
46 )
47cli = typer.Typer(rich_markup_mode="rich")
50class Styles(str, Enum):
51 bulma = "bulma"
52 webawesome = "webawesome"
53 custom = "custom"
55 def __str__(self) -> str:
56 return t.cast(str, self.value)
59run_args: dict[str, t.Any] = {"app": "main:app"}
60dev_args: dict[str, t.Any] = run_args | {"port": 8000, "reload": True}
61granian_dev_args: dict[str, t.Any] = dev_args | {
62 "address": "127.0.0.1",
63 "reload_paths": [Path.cwd(), fastblocks_path],
64 "interface": "asgi",
65 "log_enabled": False,
66 "log_access": True,
67 "reload_ignore_dirs": ["tmp", "settings", "templates"],
68}
69uvicorn_dev_args: dict[str, t.Any] = dev_args | {
70 "host": "127.0.0.1",
71 "reload_includes": ["*.py", str(Path.cwd()), str(fastblocks_path)],
72 "reload_excludes": ["tmp/*", "settings/*", "templates/*"],
73 "lifespan": "on",
74}
77def setup_signal_handlers() -> None:
78 import sys
80 def signal_handler(_signum: int, _frame: t.Any) -> None:
81 sys.exit(0)
83 signal.signal(signal.SIGINT, signal_handler)
84 signal.signal(signal.SIGTERM, signal_handler)
87@cli.command()
88def run(docker: bool = False, granian: bool = False, host: str = "127.0.0.1") -> None:
89 if docker:
90 execute(
91 f"docker run -it -ePORT=8080 -p8080:8080 {Path.cwd().stem}".split(),
92 )
93 else:
94 setup_signal_handlers()
95 if granian:
96 from granian.constants import Interfaces
98 Granian("main:app", address=host, interface=Interfaces.ASGI).serve()
99 else:
100 uvicorn.run(app=run_args["app"], host=host, lifespan="on", log_config=None)
103@cli.command()
104def dev(granian: bool = False) -> None:
105 setup_signal_handlers()
106 if granian:
107 from granian.constants import Interfaces
109 Granian(
110 "main:app",
111 address="127.0.0.1",
112 port=8000,
113 reload=True,
114 reload_paths=[Path.cwd(), fastblocks_path],
115 interface=Interfaces.ASGI,
116 log_enabled=False,
117 log_access=True,
118 ).serve()
119 else:
120 uvicorn.run(
121 app="main:app",
122 host="127.0.0.1",
123 port=8000,
124 reload=True,
125 reload_includes=["*.py", str(Path.cwd()), str(fastblocks_path)],
126 reload_excludes=["tmp/*", "settings/*", "templates/*"],
127 lifespan="on",
128 log_config=None,
129 )
132def _display_adapters() -> None:
133 from acb.adapters import get_adapters
135 console.print("[bold green]Available Adapters:[/bold green]")
136 adapters = get_adapters()
137 if not adapters:
138 console.print(" [dim]No adapters found[/dim]")
139 return
140 categories: dict[str, list[t.Any]] = {}
141 for adapter in adapters:
142 if adapter.category not in categories:
143 categories[adapter.category] = []
144 categories[adapter.category].append(adapter)
145 for category in sorted(categories.keys()):
146 console.print(f"\n [bold cyan]{category.upper()}:[/bold cyan]")
147 for adapter in sorted(categories[category], key=lambda a: a.name):
148 _display_adapter_info(adapter)
151def _display_adapter_info(adapter: t.Any) -> None:
152 status = "[green]✓[/green]" if adapter.installed else "[red]✗[/red]"
153 enabled = "[yellow]enabled[/yellow]" if adapter.enabled else "[dim]disabled[/dim]"
154 console.print(f" {status} [white]{adapter.name}[/white] - {enabled}")
155 if adapter.module:
156 console.print(f" [dim]{adapter.module}[/dim]")
159def _display_default_config() -> None:
160 console.print("\n[bold green]FastBlocks Default Configuration:[/bold green]")
161 for category, default_name in default_adapters.items():
162 console.print(f" [cyan]{category}[/cyan]: [white]{default_name}[/white]")
165def _display_actions() -> None:
166 console.print("\n[bold green]FastBlocks Actions:[/bold green]")
167 try:
168 from fastblocks.actions.minify import minify
170 console.print(" [cyan]minify[/cyan]:")
171 console.print(f" [white]- css[/white] ([dim]{minify.css.__name__}[/dim])")
172 console.print(f" [white]- js[/white] ([dim]{minify.js.__name__}[/dim])")
173 except ImportError:
174 console.print(" [dim]Minify actions not available[/dim]")
177@cli.command()
178def components() -> None:
179 try:
180 console.print("\n[bold blue]FastBlocks Components[/bold blue]\n")
181 _display_adapters()
182 _display_default_config()
183 _display_actions()
184 _display_htmy_commands()
185 except Exception as e:
186 console.print(f"[red]Error displaying components: {e}[/red]")
187 console.print("[dim]Make sure you're in a FastBlocks project directory[/dim]")
190def _display_htmy_commands() -> None:
191 console.print("\n[bold green]HTMY Component Commands:[/bold green]")
192 console.print(" [cyan]scaffold[/cyan]: Create new HTMY components")
193 console.print(" [cyan]list[/cyan]: List all discovered components")
194 console.print(" [cyan]validate[/cyan]: Validate component structure")
195 console.print(" [cyan]info[/cyan]: Get component metadata")
198# Component scaffolding helpers
199_COMPONENT_TYPE_MAP = {
200 "basic": "BASIC",
201 "dataclass": "DATACLASS",
202 "htmx": "HTMX",
203 "composite": "COMPOSITE",
204}
206_TYPE_MAP = {
207 "str": str,
208 "int": int,
209 "float": float,
210 "bool": bool,
211 "list": list,
212 "dict": dict,
213}
216def _parse_component_type(type_str: str) -> t.Any:
217 """Parse component type string to ComponentType enum."""
218 from fastblocks.adapters.templates._htmy_components import ComponentType
220 type_name = _COMPONENT_TYPE_MAP.get(type_str.lower(), "DATACLASS")
221 return getattr(ComponentType, type_name)
224def _parse_component_props(props: str) -> dict[str, type]:
225 """Parse props string into dict of name:type pairs."""
226 if not props:
227 return {}
229 parsed = {}
230 for prop_def in props.split(","):
231 if ":" not in prop_def:
232 continue
233 prop_name, prop_type = prop_def.strip().split(":", 1)
234 parsed[prop_name] = _TYPE_MAP.get(prop_type, str)
235 return parsed
238def _parse_component_children(children: str) -> list[str] | None:
239 """Parse children string into list of component names."""
240 if not children:
241 return None
242 return [c.strip() for c in children.split(",") if c.strip()]
245def _build_scaffold_kwargs(
246 parsed_props: dict[str, t.Any],
247 htmx: bool,
248 component_type: t.Any,
249 endpoint: str,
250 trigger: str,
251 target: str,
252 parsed_children: list[str] | None,
253) -> dict[str, t.Any]:
254 """Build kwargs dict for component scaffolding."""
255 from fastblocks.adapters.templates._htmy_components import ComponentType
257 kwargs: dict[str, t.Any] = {}
259 if parsed_props:
260 kwargs["props"] = parsed_props
262 if htmx or component_type == ComponentType.HTMX:
263 kwargs["htmx_enabled"] = True
264 if endpoint:
265 kwargs["endpoint"] = endpoint
266 kwargs["trigger"] = trigger
267 kwargs["target"] = target
269 if parsed_children:
270 kwargs["children"] = parsed_children
272 return kwargs
275@cli.command()
276def scaffold(
277 name: Annotated[str, typer.Argument(help="Component name")],
278 type: Annotated[
279 str,
280 typer.Option(
281 "--type", "-t", help="Component type: basic, dataclass, htmx, composite"
282 ),
283 ] = "dataclass",
284 htmx: Annotated[
285 bool, typer.Option("--htmx", "-x", help="Enable HTMX features")
286 ] = False,
287 endpoint: Annotated[
288 str, typer.Option("--endpoint", "-e", help="HTMX endpoint URL")
289 ] = "",
290 trigger: Annotated[
291 str, typer.Option("--trigger", help="HTMX trigger event")
292 ] = "click",
293 target: Annotated[
294 str, typer.Option("--target", help="HTMX target selector")
295 ] = "#content",
296 props: Annotated[
297 str,
298 typer.Option("--props", "-p", help="Component props as 'name:type,name:type'"),
299 ] = "",
300 children: Annotated[
301 str, typer.Option("--children", "-c", help="Child components as 'comp1,comp2'")
302 ] = "",
303 path: Annotated[str, typer.Option("--path", help="Custom component path")] = "",
304) -> None:
305 """Scaffold a new HTMY component."""
306 import asyncio
307 from pathlib import Path
309 async def scaffold_component() -> None:
310 try:
311 from acb.depends import depends
313 # Get HTMY adapter
314 htmy_adapter = depends.get("htmy")
315 if htmy_adapter is None:
316 console.print(
317 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
318 )
319 return
321 # Parse inputs using helper functions
322 component_type = _parse_component_type(type)
323 parsed_props = _parse_component_props(props)
324 parsed_children = _parse_component_children(children)
326 # Build kwargs
327 kwargs = _build_scaffold_kwargs(
328 parsed_props,
329 htmx,
330 component_type,
331 endpoint,
332 trigger,
333 target,
334 parsed_children,
335 )
337 # Custom path
338 target_path = Path(path) if path else None
340 # Scaffold component
341 created_path = await htmy_adapter.scaffold_component(
342 name=name,
343 component_type=component_type,
344 target_path=target_path,
345 **kwargs,
346 )
348 console.print(
349 f"[green]✓[/green] Created {component_type.value} component '{name}' at {created_path}"
350 )
352 except Exception as e:
353 console.print(f"[red]Error scaffolding component: {e}[/red]")
355 asyncio.run(scaffold_component())
358def _get_component_status_color(status_value: str) -> str:
359 """Get the color for a component status."""
360 return {
361 "discovered": "yellow",
362 "validated": "green",
363 "compiled": "blue",
364 "ready": "green",
365 "error": "red",
366 "deprecated": "dim",
367 }.get(status_value, "white")
370def _display_component_entry(name: str, metadata: t.Any) -> None:
371 """Display a single component entry with status and metadata."""
372 status_color = _get_component_status_color(metadata.status.value)
374 console.print(
375 f" [{status_color}]●[/{status_color}] [white]{name}[/white] ({metadata.type.value})"
376 )
377 console.print(f" [dim]{metadata.path}[/dim]")
379 if metadata.error_message:
380 console.print(f" [red]Error: {metadata.error_message}[/red]")
381 elif metadata.docstring:
382 # Show first line of docstring
383 first_line = metadata.docstring.split("\n")[0].strip()
384 if first_line:
385 console.print(f" [dim]{first_line}[/dim]")
388@cli.command(name="list")
389def list_components() -> None:
390 """List all discovered HTMY components."""
391 import asyncio
393 async def list_all_components() -> None:
394 try:
395 from acb.depends import depends
397 htmy_adapter = depends.get("htmy")
398 if htmy_adapter is None:
399 console.print(
400 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
401 )
402 return
404 components = await htmy_adapter.discover_components()
406 if not components:
407 console.print("[dim]No components found.[/dim]")
408 return
410 console.print(
411 f"\n[bold green]Found {len(components)} HTMY components:[/bold green]\n"
412 )
414 for name, metadata in components.items():
415 _display_component_entry(name, metadata)
417 except Exception as e:
418 console.print(f"[red]Error listing components: {e}[/red]")
420 asyncio.run(list_all_components())
423def _display_basic_metadata(component: str, metadata: t.Any) -> None:
424 """Display basic component metadata."""
425 console.print(f"\n[bold blue]Component: {component}[/bold blue]")
426 console.print(f" [cyan]Type:[/cyan] {metadata.type.value}")
427 console.print(f" [cyan]Status:[/cyan] {metadata.status.value}")
428 console.print(f" [cyan]Path:[/cyan] {metadata.path}")
431def _display_optional_metadata(metadata: t.Any) -> None:
432 """Display optional component metadata fields."""
433 if metadata.last_modified:
434 console.print(f" [cyan]Modified:[/cyan] {metadata.last_modified}")
436 if metadata.docstring:
437 console.print(f" [cyan]Description:[/cyan] {metadata.docstring}")
440def _display_htmx_attributes(metadata: t.Any) -> None:
441 """Display HTMX attributes if present."""
442 if metadata.htmx_attributes:
443 console.print(" [cyan]HTMX Attributes:[/cyan]")
444 for key, value in metadata.htmx_attributes.items():
445 console.print(f" {key}: {value}")
448def _display_dependencies(metadata: t.Any) -> None:
449 """Display component dependencies if present."""
450 if metadata.dependencies:
451 console.print(
452 f" [cyan]Dependencies:[/cyan] {', '.join(metadata.dependencies)}"
453 )
456def _display_status_message(metadata: t.Any) -> None:
457 """Display status-specific message."""
458 if metadata.status.value == "error":
459 console.print(f" [red]Error:[/red] {metadata.error_message}")
460 elif metadata.status.value == "ready":
461 console.print(" [green]✓ Component is ready[/green]")
464@cli.command()
465def validate(
466 component: Annotated[str, typer.Argument(help="Component name to validate")],
467) -> None:
468 """Validate a specific HTMY component."""
469 import asyncio
471 async def validate_component() -> None:
472 try:
473 from acb.depends import depends
475 htmy_adapter = depends.get("htmy")
476 if htmy_adapter is None:
477 console.print(
478 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
479 )
480 return
482 metadata = await htmy_adapter.validate_component(component)
484 _display_basic_metadata(component, metadata)
485 _display_optional_metadata(metadata)
486 _display_htmx_attributes(metadata)
487 _display_dependencies(metadata)
488 _display_status_message(metadata)
490 except Exception as e:
491 console.print(f"[red]Error validating component '{component}': {e}[/red]")
493 asyncio.run(validate_component())
496@cli.command()
497def info(
498 component: Annotated[str, typer.Argument(help="Component name to get info for")],
499) -> None:
500 """Get detailed information about an HTMY component."""
501 import asyncio
503 def _display_component_class_info(
504 component_class: type, component_name: str
505 ) -> None:
506 """Display component class information including dataclass fields and HTMX status."""
507 console.print(f"\n[bold blue]Component: {component_name}[/bold blue]")
508 console.print(f" [cyan]Class:[/cyan] {component_class.__name__}")
509 console.print(f" [cyan]Module:[/cyan] {component_class.__module__}")
511 # Check if it's a dataclass
512 from dataclasses import fields, is_dataclass
514 if is_dataclass(component_class):
515 console.print(" [cyan]Fields:[/cyan]")
516 for field in fields(component_class):
517 console.print(f" {field.name}: {field.type}")
519 # Check for HTMX mixin
520 from fastblocks.adapters.templates._htmy_components import HTMXComponentMixin
522 if issubclass(component_class, HTMXComponentMixin):
523 console.print(" [cyan]HTMX Enabled:[/cyan] Yes")
525 def _display_component_metadata(metadata: t.Any) -> None:
526 """Display component metadata information."""
527 console.print(f" [cyan]Type:[/cyan] {metadata.type.value}")
528 console.print(f" [cyan]Status:[/cyan] {metadata.status.value}")
529 console.print(f" [cyan]Path:[/cyan] {metadata.path}")
531 async def get_component_info() -> None:
532 try:
533 from acb.depends import depends
535 htmy_adapter = depends.get("htmy")
536 if htmy_adapter is None:
537 console.print(
538 "[red]HTMY adapter not found. Make sure you're in a FastBlocks project.[/red]"
539 )
540 return
542 # Get component metadata
543 metadata = await htmy_adapter.validate_component(component)
545 # Try to get component class for more info
546 try:
547 component_class = await htmy_adapter.get_component_class(component)
548 _display_component_class_info(component_class, component)
549 except Exception as e:
550 console.print(f"[red]Could not load component class: {e}[/red]")
552 # Show metadata
553 _display_component_metadata(metadata)
555 except Exception as e:
556 console.print(
557 f"[red]Error getting info for component '{component}': {e}[/red]"
558 )
560 asyncio.run(get_component_info())
563def _get_severity_color(severity: str) -> str:
564 """Get the color for an error severity level."""
565 return {
566 "error": "red",
567 "warning": "yellow",
568 "info": "blue",
569 "hint": "dim",
570 }.get(severity, "white")
573def _display_syntax_error(error: t.Any) -> None:
574 """Display a single syntax error with formatting."""
575 severity_color = _get_severity_color(error.severity)
577 console.print(
578 f" [{severity_color}]{error.severity.upper()}[/{severity_color}] "
579 f"Line {error.line + 1}, Column {error.column + 1}: {error.message}"
580 )
582 if error.fix_suggestion:
583 console.print(f" [dim]Fix: {error.fix_suggestion}[/dim]")
585 if error.code:
586 console.print(f" [dim]Code: {error.code}[/dim]")
589def _display_syntax_errors(file_path: str, errors: list[t.Any]) -> None:
590 """Display all syntax errors for a file."""
591 console.print(f"\n[bold red]Syntax errors found in {file_path}:[/bold red]")
592 for error in errors:
593 _display_syntax_error(error)
596@cli.command()
597def syntax_check(
598 file_path: Annotated[
599 str, typer.Argument(help="Path to FastBlocks template file to check")
600 ],
601 format_output: Annotated[
602 bool, typer.Option("--format", help="Format the output for better readability")
603 ] = False,
604) -> None:
605 """Check FastBlocks template syntax for errors and warnings."""
606 import asyncio
608 async def check_syntax() -> None:
609 try:
610 from pathlib import Path
612 from acb.depends import depends
614 syntax_support = depends.get("syntax_support")
615 if syntax_support is None:
616 console.print(
617 "[red]Syntax support not available. Make sure you're in a FastBlocks project.[/red]"
618 )
619 return
621 template_path = Path(file_path)
622 if not template_path.exists():
623 console.print(f"[red]File not found: {file_path}[/red]")
624 return
626 content = template_path.read_text()
627 errors = syntax_support.check_syntax(content, template_path)
629 if not errors:
630 console.print(f"[green]✓ No syntax errors found in {file_path}[/green]")
631 return
633 _display_syntax_errors(file_path, errors)
635 except Exception as e:
636 console.print(f"[red]Error checking syntax: {e}[/red]")
638 asyncio.run(check_syntax())
641@cli.command()
642def format_template(
643 file_path: Annotated[
644 str, typer.Argument(help="Path to FastBlocks template file to format")
645 ],
646 in_place: Annotated[
647 bool, typer.Option("--in-place", "-i", help="Format file in place")
648 ] = False,
649) -> None:
650 """Format a FastBlocks template file."""
651 import asyncio
653 async def format_file() -> None:
654 try:
655 from pathlib import Path
657 from acb.depends import depends
659 syntax_support = depends.get("syntax_support")
660 if syntax_support is None:
661 console.print(
662 "[red]Syntax support not available. Make sure you're in a FastBlocks project.[/red]"
663 )
664 return
666 template_path = Path(file_path)
667 if not template_path.exists():
668 console.print(f"[red]File not found: {file_path}[/red]")
669 return
671 content = template_path.read_text()
672 formatted = syntax_support.format_template(content)
674 if formatted == content:
675 console.print(f"[green]✓ File {file_path} is already formatted[/green]")
676 return
678 if in_place:
679 template_path.write_text(formatted)
680 console.print(f"[green]✓ Formatted {file_path} in place[/green]")
681 else:
682 console.print(formatted)
684 except Exception as e:
685 console.print(f"[red]Error formatting template: {e}[/red]")
687 asyncio.run(format_file())
690@cli.command()
691def generate_ide_config(
692 output_dir: Annotated[
693 str,
694 typer.Option("--output", "-o", help="Output directory for IDE configurations"),
695 ] = ".vscode",
696 ide: Annotated[
697 str, typer.Option(help="IDE to generate config for (vscode, vim, emacs)")
698 ] = "vscode",
699) -> None:
700 """Generate IDE configuration files for FastBlocks syntax support."""
701 import asyncio
702 import json
704 async def generate_config() -> None:
705 try:
706 from pathlib import Path
708 output_path = Path(output_dir)
709 output_path.mkdir(exist_ok=True)
711 if ide == "vscode":
712 # Generate VS Code extension configuration
713 from fastblocks.adapters.templates._language_server import (
714 generate_textmate_grammar,
715 generate_vscode_extension,
716 )
718 # Package.json for extension
719 package_json = generate_vscode_extension()
720 (output_path / "package.json").write_text(
721 json.dumps(package_json, indent=2)
722 )
724 # TextMate grammar
725 grammar = generate_textmate_grammar()
726 syntaxes_dir = output_path / "syntaxes"
727 syntaxes_dir.mkdir(exist_ok=True)
728 (syntaxes_dir / "fastblocks.tmLanguage.json").write_text(
729 json.dumps(grammar, indent=2)
730 )
732 # Language configuration
733 lang_config = {
734 "comments": {"blockComment": ["[#", "#]"]},
735 "brackets": [["[[", "]]"], ["[%", "%]"], ["[#", "#]"]],
736 "autoClosingPairs": [
737 {"open": "[[", "close": "]]"},
738 {"open": "[%", "close": "%]"},
739 {"open": "[#", "close": "#]"},
740 {"open": '"', "close": '"'},
741 {"open": "'", "close": "'"},
742 ],
743 "surroundingPairs": [
744 ["[[", "]]"],
745 ["[%", "%]"],
746 ["[#", "#]"],
747 ['"', '"'],
748 ["'", "'"],
749 ],
750 }
751 (output_path / "language-configuration.json").write_text(
752 json.dumps(lang_config, indent=2)
753 )
755 # Settings for FastBlocks language server
756 settings = {
757 "fastblocks.languageServer.enabled": True,
758 "fastblocks.languageServer.port": 7777,
759 "fastblocks.completion.enabled": True,
760 "fastblocks.diagnostics.enabled": True,
761 "files.associations": {
762 "*.fb.html": "fastblocks",
763 "*.fastblocks": "fastblocks",
764 },
765 }
766 (output_path / "settings.json").write_text(
767 json.dumps(settings, indent=2)
768 )
770 console.print(
771 f"[green]✓ VS Code configuration generated in {output_path}[/green]"
772 )
773 console.print(" Files created:")
774 console.print(" - package.json (extension manifest)")
775 console.print(" - language-configuration.json")
776 console.print(" - syntaxes/fastblocks.tmLanguage.json")
777 console.print(" - settings.json")
779 elif ide == "vim":
780 # Generate Vim configuration
781 vim_syntax = """
782" Vim syntax file for FastBlocks templates
783" Language: FastBlocks
784" Maintainer: FastBlocks Team
786if exists("b:current_syntax")
787 finish
788endif
790" FastBlocks delimiters
791syn region fastblocksVariable start="\\[\\[" end="\\]\\]" contains=fastblocksFilter,fastblocksString
792syn region fastblocksBlock start="\\[%" end="%\\]" contains=fastblocksKeyword,fastblocksString
793syn region fastblocksComment start="\\[#" end="#\\]"
795" Keywords
796syn keyword fastblocksKeyword if else elif endif for endfor block endblock extends include set macro endmacro
798" Filters
799syn match fastblocksFilter "|\\s*\\w\\+" contained
801" Strings
802syn region fastblocksString start='"' end='"' contained
803syn region fastblocksString start="'" end="'" contained
805" Highlighting
806hi def link fastblocksVariable Special
807hi def link fastblocksBlock Keyword
808hi def link fastblocksComment Comment
809hi def link fastblocksKeyword Statement
810hi def link fastblocksFilter Function
811hi def link fastblocksString String
813let b:current_syntax = "fastblocks"
814"""
815 (output_path / "fastblocks.vim").write_text(vim_syntax)
816 console.print(
817 f"[green]✓ Vim syntax file generated: {output_path}/fastblocks.vim[/green]"
818 )
820 elif ide == "emacs":
821 # Generate Emacs configuration
822 emacs_mode = """
823;;; fastblocks-mode.el --- Major mode for FastBlocks templates
825(defvar fastblocks-mode-syntax-table
826 (let ((table (make-syntax-table)))
827 (modify-syntax-entry ?\" "\\\"" table)
828 (modify-syntax-entry ?\' "\\\"" table)
829 table)
830 "Syntax table for `fastblocks-mode'.")
832(defvar fastblocks-font-lock-keywords
833 '(("\\\\[\\\\[.*?\\\\]\\\\]" . font-lock-variable-name-face)
834 ("\\\\[%.*?%\\\\]" . font-lock-keyword-face)
835 ("\\\\[#.*?#\\\\]" . font-lock-comment-face)
836 ("\\\\b\\\\(if\\\\|else\\\\|elif\\\\|endif\\\\|for\\\\|endfor\\\\|block\\\\|endblock\\\\|extends\\\\|include\\\\|set\\\\|macro\\\\|endmacro\\\\)\\\\b" . font-lock-builtin-face))
837 "Font lock keywords for FastBlocks mode.")
839(define-derived-mode fastblocks-mode html-mode "FastBlocks"
840 "Major mode for editing FastBlocks templates."
841 (setq font-lock-defaults '(fastblocks-font-lock-keywords)))
843(add-to-list 'auto-mode-alist '("\\\\.fb\\\\.html\\\\'" . fastblocks-mode))
844(add-to-list 'auto-mode-alist '("\\\\.fastblocks\\\\'" . fastblocks-mode))
846(provide 'fastblocks-mode)
847;;; fastblocks-mode.el ends here
848"""
849 (output_path / "fastblocks-mode.el").write_text(emacs_mode)
850 console.print(
851 f"[green]✓ Emacs mode file generated: {output_path}/fastblocks-mode.el[/green]"
852 )
854 else:
855 console.print(f"[red]Unsupported IDE: {ide}[/red]")
856 console.print("Supported IDEs: vscode, vim, emacs")
858 except Exception as e:
859 console.print(f"[red]Error generating IDE configuration: {e}[/red]")
861 asyncio.run(generate_config())
864@cli.command()
865def start_language_server(
866 port: Annotated[
867 int, typer.Option("--port", "-p", help="Port to run language server on")
868 ] = 7777,
869 host: Annotated[
870 str, typer.Option("--host", help="Host to bind language server to")
871 ] = "localhost",
872 stdio: Annotated[
873 bool, typer.Option("--stdio", help="Use stdio instead of TCP")
874 ] = False,
875) -> None:
876 """Start the FastBlocks Language Server."""
877 import asyncio
879 async def start_server() -> None:
880 try:
881 from acb.depends import depends
883 language_server = depends.get("language_server")
884 if language_server is None:
885 console.print(
886 "[red]Language server not available. Make sure you're in a FastBlocks project.[/red]"
887 )
888 return
890 if stdio:
891 console.print(
892 "[blue]Starting FastBlocks Language Server in stdio mode...[/blue]"
893 )
894 # In a real implementation, this would handle stdio communication
895 console.print(
896 "[yellow]Stdio mode not yet implemented. Use TCP mode.[/yellow]"
897 )
898 else:
899 console.print(
900 f"[blue]Starting FastBlocks Language Server on {host}:{port}...[/blue]"
901 )
902 console.print("[green]Language Server started successfully![/green]")
903 console.print(f"Connect your IDE to: {host}:{port}")
905 # Keep server running
906 try:
907 while True:
908 await asyncio.sleep(1)
909 except KeyboardInterrupt:
910 console.print("\n[yellow]Language server stopped.[/yellow]")
912 except Exception as e:
913 console.print(f"[red]Error starting language server: {e}[/red]")
915 asyncio.run(start_server())
918@cli.command()
919def create(
920 app_name: Annotated[
921 str,
922 typer.Option(prompt=True, help="Name of your application"),
923 ],
924 style: Annotated[
925 Styles,
926 typer.Option(
927 prompt=True,
928 help="The style (css, or web component, framework) you want to use[{','.join(Styles._member_names_)}]",
929 ),
930 ] = Styles.bulma,
931 domain: Annotated[
932 str,
933 typer.Option(prompt=True, help="Application domain"),
934 ] = "example.com",
935) -> None:
936 app_path = apps_path / app_name
937 app_path.mkdir(exist_ok=True)
938 os.chdir(app_path)
939 templates = Path("templates")
940 for p in (
941 templates / "base/blocks",
942 templates / f"{style}/blocks",
943 templates / f"{style}/theme",
944 Path("adapters"),
945 Path("actions"),
946 ):
947 p.mkdir(parents=True, exist_ok=True)
948 for p in (
949 Path("models.py"),
950 Path("routes.py"),
951 Path("main.py"),
952 Path(".envrc"),
953 Path("pyproject.toml"),
954 Path("__init__.py"),
955 Path("adapters/__init__.py"),
956 Path("actions/__init__.py"),
957 ):
958 p.touch()
959 for template_file in (
960 "main.py.tmpl",
961 ".envrc",
962 "pyproject.toml.tmpl",
963 "Procfile.tmpl",
964 ):
965 template_path = Path(template_file)
966 target_path = Path(template_file.replace(".tmpl", ""))
967 target_path.write_text(
968 (fastblocks_path / template_path).read_text().replace("APP_NAME", app_name),
969 )
970 commands = (
971 ["direnv", "allow", "."],
972 ["pdm", "install"],
973 ["python", "-m", "fastblocks", "run"],
974 )
975 for command in commands:
976 execute(command, stdout=DEVNULL, stderr=DEVNULL)
978 async def update_settings(settings: str, values: dict[str, t.Any]) -> None:
979 settings_path = AsyncPath(app_path / "settings")
980 settings_dict = await load.yaml(settings_path / f"{settings}.yml")
981 settings_dict.update(values)
982 await dump.yaml(settings_dict, settings_path / f"{settings}.yml")
984 async def update_configs() -> None:
985 await update_settings("debug", {"fastblocks": False})
986 await update_settings("adapters", default_adapters)
987 await update_settings(
988 "app", {"title": "Welcome to FastBlocks", "domain": domain}
989 )
991 asyncio.run(update_configs())
992 console.print(
993 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.[/][/]",
994 )
995 console.print(
996 "\n[dim]Use [white]`python -m fastblocks components`[/white] to see available adapters and actions.[/dim]",
997 )
998 raise SystemExit
1001@cli.command()
1002def version() -> None:
1003 try:
1004 __version__ = get_version("fastblocks")
1005 console.print(f"FastBlocks v{__version__}")
1006 except Exception:
1007 console.print("Unable to determine FastBlocks version")
1010@cli.command()
1011def mcp(
1012 port: Annotated[
1013 int,
1014 typer.Option("--port", "-p", help="Port for MCP server (default: auto)"),
1015 ] = 0,
1016 host: Annotated[
1017 str, typer.Option("--host", help="Host to bind MCP server to")
1018 ] = "localhost",
1019) -> None:
1020 """Start the FastBlocks MCP (Model Context Protocol) server.
1022 Enables IDE/AI assistant integration for FastBlocks development
1023 including template management, component creation, and adapter configuration.
1024 """
1025 import asyncio
1027 async def start_mcp_server() -> None:
1028 try:
1029 console.print("\n[bold blue]FastBlocks MCP Server[/bold blue]")
1030 console.print("[dim]Model Context Protocol for IDE Integration[/dim]\n")
1032 from .mcp import create_fastblocks_mcp_server
1034 console.print("[yellow]Initializing MCP server...[/yellow]")
1035 server = await create_fastblocks_mcp_server()
1037 console.print("[green]✓ MCP server initialized[/green]")
1039 if port:
1040 console.print(f"[blue]Starting server on {host}:{port}...[/blue]")
1041 else:
1042 console.print("[blue]Starting server...[/blue]")
1044 console.print("[green]✓ FastBlocks MCP server running[/green]")
1045 console.print("\n[dim]Press Ctrl+C to stop[/dim]\n")
1047 await server.start()
1049 except KeyboardInterrupt:
1050 console.print("\n[yellow]MCP server stopped by user[/yellow]")
1051 except ImportError as e:
1052 console.print(f"[red]MCP dependencies not available: {e}[/red]")
1053 console.print(
1054 "[dim]Make sure ACB is installed with MCP support (acb>=0.23.0)[/dim]"
1055 )
1056 except Exception as e:
1057 console.print(f"[red]Error starting MCP server: {e}[/red]")
1059 asyncio.run(start_mcp_server())