Coverage for fastblocks/cli.py: 0%

468 statements  

« 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 

13 

14with suppress(ImportError): 

15 from acb.logger import InterceptHandler 

16 

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 

23 

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 

31 

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") 

48 

49 

50class Styles(str, Enum): 

51 bulma = "bulma" 

52 webawesome = "webawesome" 

53 custom = "custom" 

54 

55 def __str__(self) -> str: 

56 return t.cast(str, self.value) 

57 

58 

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} 

75 

76 

77def setup_signal_handlers() -> None: 

78 import sys 

79 

80 def signal_handler(_signum: int, _frame: t.Any) -> None: 

81 sys.exit(0) 

82 

83 signal.signal(signal.SIGINT, signal_handler) 

84 signal.signal(signal.SIGTERM, signal_handler) 

85 

86 

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 

97 

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) 

101 

102 

103@cli.command() 

104def dev(granian: bool = False) -> None: 

105 setup_signal_handlers() 

106 if granian: 

107 from granian.constants import Interfaces 

108 

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 ) 

130 

131 

132def _display_adapters() -> None: 

133 from acb.adapters import get_adapters 

134 

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) 

149 

150 

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]") 

157 

158 

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]") 

163 

164 

165def _display_actions() -> None: 

166 console.print("\n[bold green]FastBlocks Actions:[/bold green]") 

167 try: 

168 from fastblocks.actions.minify import minify 

169 

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]") 

175 

176 

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]") 

188 

189 

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") 

196 

197 

198# Component scaffolding helpers 

199_COMPONENT_TYPE_MAP = { 

200 "basic": "BASIC", 

201 "dataclass": "DATACLASS", 

202 "htmx": "HTMX", 

203 "composite": "COMPOSITE", 

204} 

205 

206_TYPE_MAP = { 

207 "str": str, 

208 "int": int, 

209 "float": float, 

210 "bool": bool, 

211 "list": list, 

212 "dict": dict, 

213} 

214 

215 

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 

219 

220 type_name = _COMPONENT_TYPE_MAP.get(type_str.lower(), "DATACLASS") 

221 return getattr(ComponentType, type_name) 

222 

223 

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 {} 

228 

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 

236 

237 

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()] 

243 

244 

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 

256 

257 kwargs: dict[str, t.Any] = {} 

258 

259 if parsed_props: 

260 kwargs["props"] = parsed_props 

261 

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 

268 

269 if parsed_children: 

270 kwargs["children"] = parsed_children 

271 

272 return kwargs 

273 

274 

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 

308 

309 async def scaffold_component() -> None: 

310 try: 

311 from acb.depends import depends 

312 

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 

320 

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) 

325 

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 ) 

336 

337 # Custom path 

338 target_path = Path(path) if path else None 

339 

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 ) 

347 

348 console.print( 

349 f"[green]✓[/green] Created {component_type.value} component '{name}' at {created_path}" 

350 ) 

351 

352 except Exception as e: 

353 console.print(f"[red]Error scaffolding component: {e}[/red]") 

354 

355 asyncio.run(scaffold_component()) 

356 

357 

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") 

368 

369 

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) 

373 

374 console.print( 

375 f" [{status_color}]●[/{status_color}] [white]{name}[/white] ({metadata.type.value})" 

376 ) 

377 console.print(f" [dim]{metadata.path}[/dim]") 

378 

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]") 

386 

387 

388@cli.command(name="list") 

389def list_components() -> None: 

390 """List all discovered HTMY components.""" 

391 import asyncio 

392 

393 async def list_all_components() -> None: 

394 try: 

395 from acb.depends import depends 

396 

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 

403 

404 components = await htmy_adapter.discover_components() 

405 

406 if not components: 

407 console.print("[dim]No components found.[/dim]") 

408 return 

409 

410 console.print( 

411 f"\n[bold green]Found {len(components)} HTMY components:[/bold green]\n" 

412 ) 

413 

414 for name, metadata in components.items(): 

415 _display_component_entry(name, metadata) 

416 

417 except Exception as e: 

418 console.print(f"[red]Error listing components: {e}[/red]") 

419 

420 asyncio.run(list_all_components()) 

421 

422 

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}") 

429 

430 

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}") 

435 

436 if metadata.docstring: 

437 console.print(f" [cyan]Description:[/cyan] {metadata.docstring}") 

438 

439 

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}") 

446 

447 

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 ) 

454 

455 

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]") 

462 

463 

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 

470 

471 async def validate_component() -> None: 

472 try: 

473 from acb.depends import depends 

474 

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 

481 

482 metadata = await htmy_adapter.validate_component(component) 

483 

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) 

489 

490 except Exception as e: 

491 console.print(f"[red]Error validating component '{component}': {e}[/red]") 

492 

493 asyncio.run(validate_component()) 

494 

495 

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 

502 

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__}") 

510 

511 # Check if it's a dataclass 

512 from dataclasses import fields, is_dataclass 

513 

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}") 

518 

519 # Check for HTMX mixin 

520 from fastblocks.adapters.templates._htmy_components import HTMXComponentMixin 

521 

522 if issubclass(component_class, HTMXComponentMixin): 

523 console.print(" [cyan]HTMX Enabled:[/cyan] Yes") 

524 

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}") 

530 

531 async def get_component_info() -> None: 

532 try: 

533 from acb.depends import depends 

534 

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 

541 

542 # Get component metadata 

543 metadata = await htmy_adapter.validate_component(component) 

544 

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]") 

551 

552 # Show metadata 

553 _display_component_metadata(metadata) 

554 

555 except Exception as e: 

556 console.print( 

557 f"[red]Error getting info for component '{component}': {e}[/red]" 

558 ) 

559 

560 asyncio.run(get_component_info()) 

561 

562 

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") 

571 

572 

573def _display_syntax_error(error: t.Any) -> None: 

574 """Display a single syntax error with formatting.""" 

575 severity_color = _get_severity_color(error.severity) 

576 

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 ) 

581 

582 if error.fix_suggestion: 

583 console.print(f" [dim]Fix: {error.fix_suggestion}[/dim]") 

584 

585 if error.code: 

586 console.print(f" [dim]Code: {error.code}[/dim]") 

587 

588 

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) 

594 

595 

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 

607 

608 async def check_syntax() -> None: 

609 try: 

610 from pathlib import Path 

611 

612 from acb.depends import depends 

613 

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 

620 

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 

625 

626 content = template_path.read_text() 

627 errors = syntax_support.check_syntax(content, template_path) 

628 

629 if not errors: 

630 console.print(f"[green]✓ No syntax errors found in {file_path}[/green]") 

631 return 

632 

633 _display_syntax_errors(file_path, errors) 

634 

635 except Exception as e: 

636 console.print(f"[red]Error checking syntax: {e}[/red]") 

637 

638 asyncio.run(check_syntax()) 

639 

640 

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 

652 

653 async def format_file() -> None: 

654 try: 

655 from pathlib import Path 

656 

657 from acb.depends import depends 

658 

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 

665 

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 

670 

671 content = template_path.read_text() 

672 formatted = syntax_support.format_template(content) 

673 

674 if formatted == content: 

675 console.print(f"[green]✓ File {file_path} is already formatted[/green]") 

676 return 

677 

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) 

683 

684 except Exception as e: 

685 console.print(f"[red]Error formatting template: {e}[/red]") 

686 

687 asyncio.run(format_file()) 

688 

689 

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 

703 

704 async def generate_config() -> None: 

705 try: 

706 from pathlib import Path 

707 

708 output_path = Path(output_dir) 

709 output_path.mkdir(exist_ok=True) 

710 

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 ) 

717 

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 ) 

723 

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 ) 

731 

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 ) 

754 

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 ) 

769 

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") 

778 

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 

785 

786if exists("b:current_syntax") 

787 finish 

788endif 

789 

790" FastBlocks delimiters 

791syn region fastblocksVariable start="\\[\\[" end="\\]\\]" contains=fastblocksFilter,fastblocksString 

792syn region fastblocksBlock start="\\[%" end="%\\]" contains=fastblocksKeyword,fastblocksString 

793syn region fastblocksComment start="\\[#" end="#\\]" 

794 

795" Keywords 

796syn keyword fastblocksKeyword if else elif endif for endfor block endblock extends include set macro endmacro 

797 

798" Filters 

799syn match fastblocksFilter "|\\s*\\w\\+" contained 

800 

801" Strings 

802syn region fastblocksString start='"' end='"' contained 

803syn region fastblocksString start="'" end="'" contained 

804 

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 

812 

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 ) 

819 

820 elif ide == "emacs": 

821 # Generate Emacs configuration 

822 emacs_mode = """ 

823;;; fastblocks-mode.el --- Major mode for FastBlocks templates 

824 

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'.") 

831 

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.") 

838 

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))) 

842 

843(add-to-list 'auto-mode-alist '("\\\\.fb\\\\.html\\\\'" . fastblocks-mode)) 

844(add-to-list 'auto-mode-alist '("\\\\.fastblocks\\\\'" . fastblocks-mode)) 

845 

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 ) 

853 

854 else: 

855 console.print(f"[red]Unsupported IDE: {ide}[/red]") 

856 console.print("Supported IDEs: vscode, vim, emacs") 

857 

858 except Exception as e: 

859 console.print(f"[red]Error generating IDE configuration: {e}[/red]") 

860 

861 asyncio.run(generate_config()) 

862 

863 

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 

878 

879 async def start_server() -> None: 

880 try: 

881 from acb.depends import depends 

882 

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 

889 

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}") 

904 

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]") 

911 

912 except Exception as e: 

913 console.print(f"[red]Error starting language server: {e}[/red]") 

914 

915 asyncio.run(start_server()) 

916 

917 

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) 

977 

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") 

983 

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 ) 

990 

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 

999 

1000 

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") 

1008 

1009 

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. 

1021 

1022 Enables IDE/AI assistant integration for FastBlocks development 

1023 including template management, component creation, and adapter configuration. 

1024 """ 

1025 import asyncio 

1026 

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") 

1031 

1032 from .mcp import create_fastblocks_mcp_server 

1033 

1034 console.print("[yellow]Initializing MCP server...[/yellow]") 

1035 server = await create_fastblocks_mcp_server() 

1036 

1037 console.print("[green]✓ MCP server initialized[/green]") 

1038 

1039 if port: 

1040 console.print(f"[blue]Starting server on {host}:{port}...[/blue]") 

1041 else: 

1042 console.print("[blue]Starting server...[/blue]") 

1043 

1044 console.print("[green]✓ FastBlocks MCP server running[/green]") 

1045 console.print("\n[dim]Press Ctrl+C to stop[/dim]\n") 

1046 

1047 await server.start() 

1048 

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]") 

1058 

1059 asyncio.run(start_mcp_server())