Skip to content

Commit 821d9ac

Browse files
committed
feat(cli): add frontend init
1 parent 74909c7 commit 821d9ac

File tree

7 files changed

+210
-13
lines changed

7 files changed

+210
-13
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""CLI command modules for myfy."""
2+
3+
from myfy_cli.commands.frontend import frontend_app
4+
5+
__all__ = ["frontend_app"]
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Frontend CLI commands for myfy."""
2+
3+
import sys
4+
5+
import typer
6+
from rich.console import Console
7+
8+
# Check if frontend module is available
9+
try:
10+
from myfy.core.config import load_settings
11+
from myfy.frontend.config import FrontendSettings
12+
from myfy.frontend.scaffold import check_frontend_initialized, scaffold_frontend
13+
14+
HAS_FRONTEND = True
15+
except ImportError:
16+
HAS_FRONTEND = False
17+
18+
frontend_app = typer.Typer(help="Frontend development commands")
19+
console = Console()
20+
21+
22+
def _show_missing_module_error() -> None:
23+
"""Display error message when frontend module is not installed."""
24+
console.print("[red]✗ Frontend module not installed[/red]")
25+
console.print("")
26+
console.print("The myfy-frontend package is required for this command.")
27+
console.print("")
28+
console.print("[green]Install it with:[/green]")
29+
console.print(" pip install myfy-frontend")
30+
console.print("")
31+
console.print("[green]Or install all optional modules:[/green]")
32+
console.print(" pip install myfy[all]")
33+
34+
35+
def _prompt_interactive_config(
36+
settings: "FrontendSettings",
37+
templates_dir: str | None,
38+
static_dir: str | None,
39+
) -> tuple[str, str]:
40+
"""Prompt user for configuration in interactive mode."""
41+
console.print("[bold cyan]🎨 Frontend Initialization[/bold cyan]")
42+
console.print("")
43+
44+
# Prompt for templates directory
45+
if templates_dir is None:
46+
templates_dir = typer.prompt(
47+
"Templates directory",
48+
default=settings.templates_dir,
49+
show_default=True,
50+
)
51+
52+
# Prompt for static directory
53+
if static_dir is None:
54+
static_dir = typer.prompt(
55+
"Static files directory",
56+
default=settings.static_dir,
57+
show_default=True,
58+
)
59+
60+
# Show summary and confirm
61+
console.print("")
62+
console.print("[bold]Configuration:[/bold]")
63+
console.print(f" Templates: {templates_dir}")
64+
console.print(f" Static: {static_dir}")
65+
console.print("")
66+
67+
if not typer.confirm("Proceed with initialization?", default=True):
68+
console.print("Cancelled.")
69+
raise typer.Exit(0)
70+
71+
console.print("")
72+
return templates_dir, static_dir
73+
74+
75+
def _check_already_initialized(templates_dir: str, interactive: bool) -> None:
76+
"""Check if frontend is already initialized and prompt if needed."""
77+
if check_frontend_initialized(templates_dir):
78+
console.print("[yellow]⚠️ Frontend appears to be already initialized.[/yellow]")
79+
console.print(f"Found existing files in: {templates_dir}")
80+
console.print("")
81+
82+
if interactive or typer.confirm("Continue anyway?", default=False):
83+
console.print("")
84+
else:
85+
console.print("Cancelled.")
86+
raise typer.Exit(0)
87+
88+
89+
def _show_success_message() -> None:
90+
"""Display success message and next steps."""
91+
console.print("")
92+
console.print("[green]✨ Frontend initialized successfully![/green]")
93+
console.print("")
94+
console.print("[bold]Next steps:[/bold]")
95+
console.print(" 1. Add FrontendModule to your app:")
96+
console.print("")
97+
console.print(" [dim]from myfy.frontend import FrontendModule[/dim]")
98+
console.print(" [dim]app.add_module(FrontendModule())[/dim]")
99+
console.print("")
100+
console.print(" 2. Run development server:")
101+
console.print("")
102+
console.print(" [dim]myfy run[/dim]")
103+
console.print("")
104+
console.print(" 3. Edit templates in [cyan]frontend/templates/[/cyan]")
105+
console.print("")
106+
107+
108+
@frontend_app.command(name="init")
109+
def init(
110+
interactive: bool = typer.Option(
111+
False,
112+
"--interactive",
113+
"-i",
114+
help="Interactive mode with prompts for configuration",
115+
),
116+
templates_dir: str | None = typer.Option(
117+
None,
118+
"--templates-dir",
119+
help="Templates directory path (default: frontend/templates)",
120+
),
121+
static_dir: str | None = typer.Option(
122+
None,
123+
"--static-dir",
124+
help="Static files directory path (default: frontend/static)",
125+
),
126+
) -> None:
127+
"""
128+
Initialize frontend structure with Vite, Tailwind 4, and DaisyUI 5.
129+
130+
Creates:
131+
- package.json (Vite + Tailwind + DaisyUI)
132+
- vite.config.js
133+
- frontend/templates/ (Jinja2 templates)
134+
- frontend/css/ (Tailwind styles)
135+
- frontend/js/ (JavaScript modules)
136+
- .gitignore
137+
138+
Examples:
139+
myfy frontend init # Use defaults
140+
myfy frontend init -i # Interactive mode
141+
myfy frontend init --templates-dir my/templates
142+
"""
143+
# Check if frontend module is installed
144+
if not HAS_FRONTEND:
145+
_show_missing_module_error()
146+
sys.exit(1)
147+
148+
# Load settings (respects MYFY_FRONTEND_* env vars per ADR-0002)
149+
settings = load_settings(FrontendSettings)
150+
151+
# Interactive mode: prompt for configuration
152+
if interactive:
153+
templates_dir, static_dir = _prompt_interactive_config(settings, templates_dir, static_dir)
154+
155+
# Use defaults from settings if not provided
156+
templates_dir = templates_dir or settings.templates_dir
157+
static_dir = static_dir or settings.static_dir
158+
159+
# Check if already initialized
160+
_check_already_initialized(templates_dir, interactive)
161+
162+
# Run scaffolding
163+
console.print("[cyan]🎨 Initializing myfy frontend...[/cyan]")
164+
console.print("")
165+
166+
try:
167+
scaffold_frontend(_templates_dir=templates_dir, _static_dir=static_dir)
168+
_show_success_message()
169+
except Exception as e:
170+
console.print(f"[red]✗ Error initializing frontend: {e}[/red]")
171+
sys.exit(1)

packages/myfy-cli/myfy_cli/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- myfy run: Start development server
66
- myfy routes: List all routes
77
- myfy modules: Show loaded modules
8+
- myfy frontend: Frontend commands
89
"""
910

1011
import importlib.util
@@ -19,6 +20,7 @@
1920
from rich.table import Table
2021

2122
from myfy.core import Application
23+
from myfy_cli.commands import frontend_app
2224

2325
app = typer.Typer(
2426
name="myfy",
@@ -27,6 +29,9 @@
2729
)
2830
console = Console()
2931

32+
# Register command groups
33+
app.add_typer(frontend_app, name="frontend")
34+
3035

3136
def find_application():
3237
"""

packages/myfy-frontend/myfy/frontend/scaffold.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,29 @@ def scaffold_frontend( # noqa: PLR0915
3434
print("🎨 Initializing myfy frontend...")
3535

3636
# Get stubs directory from package resources
37-
try:
38-
stubs_path = files("myfy.frontend").joinpath("stubs")
39-
except (TypeError, AttributeError):
40-
# Fallback for development or editable installs
41-
package_dir = Path(__file__).parent
42-
stubs_path = package_dir.parent.parent / "stubs"
43-
44-
if not Path(str(stubs_path)).exists():
37+
# First try importlib.resources (works for installed packages)
38+
stubs_path = files("myfy.frontend").joinpath("stubs")
39+
40+
# Convert to Path and check if it exists
41+
stubs_path_resolved = Path(str(stubs_path))
42+
43+
# If not found, try relative to this file (for editable installs)
44+
if not stubs_path_resolved.exists():
45+
# In editable mode: __file__ is in source, stubs are at package root
46+
package_root = Path(__file__).parent.parent.parent
47+
stubs_path_resolved = package_root / "stubs"
48+
49+
if not stubs_path_resolved.exists():
4550
print("❌ Error: Stubs directory not found in package")
46-
print(f" Looking for: {stubs_path}")
51+
print(f" Looking for: {stubs_path_resolved}")
4752
sys.exit(1)
4853

4954
project_root = Path.cwd()
5055

5156
# Copy configuration files to project root
5257
config_files = ["package.json", "vite.config.js", ".gitignore"]
5358
for file_name in config_files:
54-
src = Path(str(stubs_path)) / file_name
59+
src = stubs_path_resolved / file_name
5560
dest = project_root / file_name
5661

5762
if dest.exists():
@@ -63,7 +68,7 @@ def scaffold_frontend( # noqa: PLR0915
6368
print(f"⚠️ Warning: {file_name} not found in stubs")
6469

6570
# Copy frontend directory structure
66-
frontend_src = Path(str(stubs_path)) / "frontend"
71+
frontend_src = stubs_path_resolved / "frontend"
6772
frontend_dest = project_root / "frontend"
6873

6974
if frontend_dest.exists():

packages/myfy/myfy/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
pip install myfy[all] # Everything
1010
"""
1111

12+
# Namespace package for myfy
13+
# See PEP 420 - Implicit Namespace Packages
14+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
15+
1216
from .version import __version__
1317

1418
__all__ = ["__version__"]

packages/myfy/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ dependencies = [
1111

1212
[project.optional-dependencies]
1313
web = ["myfy-web~=0.1.0"]
14-
all = ["myfy-web~=0.1.0"]
14+
frontend = ["myfy-frontend~=0.1.0"]
15+
all = ["myfy-web~=0.1.0", "myfy-frontend~=0.1.0"]
1516

1617
[build-system]
1718
requires = ["hatchling"]

uv.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)