elric-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. elric_cli/__init__.py +1 -0
  2. elric_cli/app.py +24 -0
  3. elric_cli/commands/.gitkeep +0 -0
  4. elric_cli/commands/__init__.py +0 -0
  5. elric_cli/commands/apikey.py +134 -0
  6. elric_cli/commands/make.py +266 -0
  7. elric_cli/commands/migrate.py +69 -0
  8. elric_cli/commands/project.py +72 -0
  9. elric_cli/commands/route.py +49 -0
  10. elric_cli/commands/serve.py +33 -0
  11. elric_cli/install.sh +92 -0
  12. elric_cli/stubs/.gitkeep +0 -0
  13. elric_cli/stubs/agent.stub.py +23 -0
  14. elric_cli/stubs/agent_chat.stub.py +62 -0
  15. elric_cli/stubs/agent_planner.stub.py +82 -0
  16. elric_cli/stubs/agent_react.stub.py +83 -0
  17. elric_cli/stubs/agent_simple.stub.py +32 -0
  18. elric_cli/stubs/agent_streaming.stub.py +59 -0
  19. elric_cli/stubs/agent_tool.stub.py +63 -0
  20. elric_cli/stubs/chain.stub.py +23 -0
  21. elric_cli/stubs/controller.stub.py +30 -0
  22. elric_cli/stubs/event.stub.py +18 -0
  23. elric_cli/stubs/exception.stub.py +21 -0
  24. elric_cli/stubs/job.stub.py +24 -0
  25. elric_cli/stubs/listener.stub.py +23 -0
  26. elric_cli/stubs/middleware.stub.py +32 -0
  27. elric_cli/stubs/migration.stub.py +26 -0
  28. elric_cli/stubs/model.stub.py +18 -0
  29. elric_cli/stubs/route.stub.py +33 -0
  30. elric_cli/stubs/schema.stub.py +32 -0
  31. elric_cli/stubs/test.stub.py +21 -0
  32. elric_cli/stubs/tool.stub.py +24 -0
  33. elric_cli/uninstall.sh +64 -0
  34. elric_cli/utils.py +181 -0
  35. elric_cli-1.0.0.dist-info/METADATA +98 -0
  36. elric_cli-1.0.0.dist-info/RECORD +39 -0
  37. elric_cli-1.0.0.dist-info/WHEEL +4 -0
  38. elric_cli-1.0.0.dist-info/entry_points.txt +2 -0
  39. elric_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
elric_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # TODO: Implementazione completa in Fase 5
elric_cli/app.py ADDED
@@ -0,0 +1,24 @@
1
+ import typer
2
+
3
+ from elric_cli.commands import apikey, make, migrate, route, serve
4
+ from elric_cli.commands.project import new_project
5
+
6
+ app = typer.Typer(
7
+ name="elric",
8
+ help="Elric CLI - bootstrap and manage Elric projects",
9
+ add_completion=False,
10
+ )
11
+
12
+ # Register command groups
13
+ app.add_typer(make.app, name="make")
14
+ app.add_typer(migrate.app, name="migrate")
15
+ app.add_typer(route.app, name="route")
16
+ app.add_typer(apikey.app, name="apikey")
17
+
18
+ # Register standalone commands
19
+ app.command()(serve.serve)
20
+ app.command("new")(new_project)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ app()
File without changes
File without changes
@@ -0,0 +1,134 @@
1
+ import asyncio
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ app = typer.Typer(help="API key management commands")
8
+ console = Console()
9
+
10
+
11
+ def _missing_project_dependency(error: Exception) -> None:
12
+ message = (
13
+ "This command must run inside an Elric application project. "
14
+ "Use `elric new <name>` to create one, then run this command in that directory."
15
+ )
16
+ typer.secho(f"āœ— {message}", fg=typer.colors.RED)
17
+ typer.secho(f" Details: {error}", fg=typer.colors.YELLOW)
18
+
19
+
20
+ @app.command("create")
21
+ def create_apikey(name: str = typer.Argument(..., help="Name for the API key")):
22
+ """Create a new API key."""
23
+ async def _create():
24
+ from app.providers.database import AsyncSessionLocal
25
+ from app.utils.api_key import create_api_key_record
26
+
27
+ async with AsyncSessionLocal() as session:
28
+ api_key_record, key = await create_api_key_record(name, session)
29
+
30
+ console.print("\n[bold green]āœ“ API Key created successfully![/bold green]\n")
31
+ console.print(f"[bold]ID:[/bold] {api_key_record.id}")
32
+ console.print(f"[bold]Name:[/bold] {api_key_record.name}")
33
+ console.print(f"[bold]Prefix:[/bold] {api_key_record.prefix}")
34
+ console.print(f"[bold]Created:[/bold] {api_key_record.created_at}")
35
+ console.print(f"[bold]Active:[/bold] {api_key_record.is_active}\n")
36
+ console.print(f"[bold yellow]šŸ”‘ API Key:[/bold yellow] {key}\n")
37
+ console.print("[bold red]āš ļø IMPORTANT:[/bold red] Save this key securely!")
38
+ console.print(" This is the only time you'll see the full key.\n")
39
+
40
+ try:
41
+ asyncio.run(_create())
42
+ except ModuleNotFoundError as e:
43
+ _missing_project_dependency(e)
44
+ raise typer.Exit(1)
45
+ except Exception as e:
46
+ typer.secho(f"āœ— Failed to create API key: {e}", fg=typer.colors.RED)
47
+ raise typer.Exit(1)
48
+
49
+
50
+ @app.command("list")
51
+ def list_apikeys():
52
+ """List all API keys."""
53
+ async def _list():
54
+ from sqlalchemy import select
55
+
56
+ from app.providers.database import AsyncSessionLocal
57
+ from database.models.api_key import ApiKey
58
+
59
+ async with AsyncSessionLocal() as session:
60
+ result = await session.execute(
61
+ select(ApiKey).order_by(ApiKey.created_at.desc())
62
+ )
63
+ api_keys = result.scalars().all()
64
+
65
+ if not api_keys:
66
+ console.print("[yellow]No API keys found.[/yellow]")
67
+ return
68
+
69
+ table = Table(title="API Keys")
70
+ table.add_column("ID", style="cyan")
71
+ table.add_column("Name", style="green")
72
+ table.add_column("Prefix", style="yellow")
73
+ table.add_column("Active", style="magenta")
74
+ table.add_column("Created", style="blue")
75
+ table.add_column("Last Used", style="white")
76
+
77
+ for key in api_keys:
78
+ table.add_row(
79
+ str(key.id)[:8] + "...",
80
+ key.name,
81
+ key.prefix,
82
+ "āœ“" if key.is_active else "āœ—",
83
+ key.created_at.strftime("%Y-%m-%d %H:%M"),
84
+ key.last_used_at.strftime("%Y-%m-%d %H:%M") if key.last_used_at else "-",
85
+ )
86
+
87
+ console.print(table)
88
+ console.print(f"\n[bold]Total API keys:[/bold] {len(api_keys)}")
89
+
90
+ try:
91
+ asyncio.run(_list())
92
+ except ModuleNotFoundError as e:
93
+ _missing_project_dependency(e)
94
+ raise typer.Exit(1)
95
+ except Exception as e:
96
+ typer.secho(f"āœ— Failed to list API keys: {e}", fg=typer.colors.RED)
97
+ raise typer.Exit(1)
98
+
99
+
100
+ @app.command("revoke")
101
+ def revoke_apikey(key_id: str = typer.Argument(..., help="API key ID to revoke")):
102
+ """Revoke an API key by ID."""
103
+ async def _revoke():
104
+ from sqlalchemy import select
105
+
106
+ from app.providers.database import AsyncSessionLocal
107
+ from database.models.api_key import ApiKey
108
+
109
+ async with AsyncSessionLocal() as session:
110
+ result = await session.execute(
111
+ select(ApiKey).where(ApiKey.id == key_id)
112
+ )
113
+ api_key = result.scalar_one_or_none()
114
+
115
+ if not api_key:
116
+ console.print(f"[red]āœ— API key not found: {key_id}[/red]")
117
+ raise typer.Exit(1)
118
+
119
+ api_key.is_active = False
120
+ session.add(api_key)
121
+ await session.commit()
122
+
123
+ console.print(f"[green]āœ“ API key revoked: {api_key.name}[/green]")
124
+
125
+ try:
126
+ asyncio.run(_revoke())
127
+ except typer.Exit:
128
+ raise
129
+ except ModuleNotFoundError as e:
130
+ _missing_project_dependency(e)
131
+ raise typer.Exit(1)
132
+ except Exception as e:
133
+ typer.secho(f"āœ— Failed to revoke API key: {e}", fg=typer.colors.RED)
134
+ raise typer.Exit(1)
@@ -0,0 +1,266 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from elric_cli.utils import (
8
+ AGENT_TYPES,
9
+ DEFAULT_MODEL,
10
+ get_agent_stub_name,
11
+ get_available_models,
12
+ get_llm_config,
13
+ get_project_root,
14
+ get_provider_from_model,
15
+ get_stub_path,
16
+ get_timestamp,
17
+ render_template,
18
+ to_kebab_case,
19
+ to_pascal_case,
20
+ to_snake_case,
21
+ validate_model,
22
+ write_file,
23
+ )
24
+
25
+ app = typer.Typer(help="Generate components from stubs")
26
+
27
+
28
+ @app.command("agent")
29
+ def make_agent(
30
+ name: str,
31
+ type: str = typer.Option("simple", "--type", "-t", help=f"Agent type: {', '.join(AGENT_TYPES.keys())}"),
32
+ model: str = typer.Option(DEFAULT_MODEL, "--model", "-m", help="LLM model to use"),
33
+ ):
34
+ """Generate a new LangGraph agent with specified type and model."""
35
+ class_name = to_pascal_case(name)
36
+ snake_name = to_snake_case(name)
37
+
38
+ # Validate agent type
39
+ if type not in AGENT_TYPES:
40
+ typer.secho(f"āœ— Invalid agent type: {type}", fg=typer.colors.RED)
41
+ typer.secho(f" Available types: {', '.join(AGENT_TYPES.keys())}", fg=typer.colors.YELLOW)
42
+ raise typer.Exit(1)
43
+
44
+ # Validate model (warn if not in predefined list)
45
+ if not validate_model(model):
46
+ typer.secho(f"⚠ Warning: '{model}' is not in the predefined model list", fg=typer.colors.YELLOW)
47
+ typer.secho(f" Available models: {', '.join(get_available_models()[:5])}...", fg=typer.colors.YELLOW)
48
+ typer.secho(f" Proceeding anyway with auto-detected provider", fg=typer.colors.YELLOW)
49
+
50
+ # Get LLM configuration
51
+ llm_config = get_llm_config(model)
52
+ provider = get_provider_from_model(model)
53
+
54
+ # Get the appropriate stub
55
+ stub_name = get_agent_stub_name(type)
56
+
57
+ context = {
58
+ "class_name": class_name,
59
+ "snake_name": snake_name,
60
+ "kebab_name": to_kebab_case(name),
61
+ "model_name": model,
62
+ "llm_import": llm_config["import"],
63
+ "llm_class": llm_config["class"],
64
+ "provider": provider,
65
+ }
66
+
67
+ content = render_template(get_stub_path(stub_name), context)
68
+ output_path = get_project_root() / "app" / "ai" / "agents" / f"{snake_name}.py"
69
+
70
+ write_file(str(output_path), content)
71
+ typer.secho(f"āœ“ Created {type} agent: {output_path}", fg=typer.colors.GREEN)
72
+ typer.secho(f" Model: {model} ({provider})", fg=typer.colors.BLUE)
73
+
74
+
75
+ @app.command("chain")
76
+ def make_chain(name: str):
77
+ """Generate a new LangChain chain."""
78
+ class_name = to_pascal_case(name)
79
+ snake_name = to_snake_case(name)
80
+
81
+ context = {
82
+ "class_name": class_name,
83
+ "snake_name": snake_name,
84
+ "kebab_name": to_kebab_case(name),
85
+ }
86
+
87
+ content = render_template(get_stub_path("chain"), context)
88
+ output_path = get_project_root() / "app" / "ai" / "chains" / f"{snake_name}.py"
89
+
90
+ write_file(str(output_path), content)
91
+ typer.secho(f"āœ“ Created chain: {output_path}", fg=typer.colors.GREEN)
92
+
93
+
94
+ @app.command("tool")
95
+ def make_tool(name: str):
96
+ """Generate a new LangChain tool."""
97
+ class_name = to_pascal_case(name)
98
+ snake_name = to_snake_case(name)
99
+
100
+ context = {
101
+ "class_name": class_name,
102
+ "snake_name": snake_name,
103
+ "kebab_name": to_kebab_case(name),
104
+ }
105
+
106
+ content = render_template(get_stub_path("tool"), context)
107
+ output_path = get_project_root() / "app" / "ai" / "tools" / f"{snake_name}.py"
108
+
109
+ write_file(str(output_path), content)
110
+ typer.secho(f"āœ“ Created tool: {output_path}", fg=typer.colors.GREEN)
111
+
112
+
113
+ @app.command("route")
114
+ def make_route(name: str):
115
+ """Generate a new FastAPI router."""
116
+ class_name = to_pascal_case(name)
117
+ snake_name = to_snake_case(name)
118
+
119
+ context = {
120
+ "class_name": class_name,
121
+ "snake_name": snake_name,
122
+ "kebab_name": to_kebab_case(name),
123
+ }
124
+
125
+ content = render_template(get_stub_path("route"), context)
126
+ output_path = get_project_root() / "app" / "routes" / f"{snake_name}.py"
127
+
128
+ write_file(str(output_path), content)
129
+ typer.secho(f"āœ“ Created route: {output_path}", fg=typer.colors.GREEN)
130
+
131
+
132
+ @app.command("controller")
133
+ def make_controller(name: str):
134
+ """Generate a new controller."""
135
+ class_name = to_pascal_case(name)
136
+ snake_name = to_snake_case(name)
137
+
138
+ context = {
139
+ "class_name": class_name,
140
+ "snake_name": snake_name,
141
+ "kebab_name": to_kebab_case(name),
142
+ }
143
+
144
+ content = render_template(get_stub_path("controller"), context)
145
+ output_path = get_project_root() / "app" / "controllers" / f"{snake_name}.py"
146
+
147
+ write_file(str(output_path), content)
148
+ typer.secho(f"āœ“ Created controller: {output_path}", fg=typer.colors.GREEN)
149
+
150
+
151
+ @app.command("schema")
152
+ def make_schema(name: str):
153
+ """Generate a new Pydantic schema."""
154
+ class_name = to_pascal_case(name)
155
+ snake_name = to_snake_case(name)
156
+
157
+ context = {
158
+ "class_name": class_name,
159
+ "snake_name": snake_name,
160
+ "kebab_name": to_kebab_case(name),
161
+ }
162
+
163
+ content = render_template(get_stub_path("schema"), context)
164
+ output_path = get_project_root() / "app" / "schemas" / f"{snake_name}.py"
165
+
166
+ write_file(str(output_path), content)
167
+ typer.secho(f"āœ“ Created schema: {output_path}", fg=typer.colors.GREEN)
168
+
169
+
170
+ @app.command("model")
171
+ def make_model(name: str):
172
+ """Generate a new SQLModel entity."""
173
+ class_name = to_pascal_case(name)
174
+ snake_name = to_snake_case(name)
175
+
176
+ context = {
177
+ "class_name": class_name,
178
+ "snake_name": snake_name,
179
+ "kebab_name": to_kebab_case(name),
180
+ }
181
+
182
+ content = render_template(get_stub_path("model"), context)
183
+ output_path = get_project_root() / "database" / "models" / f"{snake_name}.py"
184
+
185
+ write_file(str(output_path), content)
186
+ typer.secho(f"āœ“ Created model: {output_path}", fg=typer.colors.GREEN)
187
+
188
+
189
+ @app.command("migration")
190
+ def make_migration(description: str):
191
+ """Generate a new Alembic migration."""
192
+ try:
193
+ result = subprocess.run(
194
+ ["alembic", "revision", "--autogenerate", "-m", description],
195
+ cwd=get_project_root(),
196
+ capture_output=True,
197
+ text=True,
198
+ )
199
+
200
+ if result.returncode == 0:
201
+ typer.secho(f"āœ“ Created migration: {description}", fg=typer.colors.GREEN)
202
+ typer.echo(result.stdout)
203
+ else:
204
+ typer.secho(f"āœ— Failed to create migration", fg=typer.colors.RED)
205
+ typer.echo(result.stderr)
206
+ raise typer.Exit(1)
207
+ except FileNotFoundError:
208
+ typer.secho("āœ— Alembic not found. Make sure it's installed.", fg=typer.colors.RED)
209
+ raise typer.Exit(1)
210
+
211
+
212
+ @app.command("job")
213
+ def make_job(name: str):
214
+ """Generate a new background job."""
215
+ class_name = to_pascal_case(name)
216
+ snake_name = to_snake_case(name)
217
+
218
+ context = {
219
+ "class_name": class_name,
220
+ "snake_name": snake_name,
221
+ "kebab_name": to_kebab_case(name),
222
+ }
223
+
224
+ content = render_template(get_stub_path("job"), context)
225
+ output_path = get_project_root() / "app" / "jobs" / f"{snake_name}.py"
226
+
227
+ write_file(str(output_path), content)
228
+ typer.secho(f"āœ“ Created job: {output_path}", fg=typer.colors.GREEN)
229
+
230
+
231
+ @app.command("exception")
232
+ def make_exception(name: str):
233
+ """Generate a new custom exception."""
234
+ class_name = to_pascal_case(name)
235
+ snake_name = to_snake_case(name)
236
+
237
+ context = {
238
+ "class_name": class_name,
239
+ "snake_name": snake_name,
240
+ "kebab_name": to_kebab_case(name),
241
+ }
242
+
243
+ content = render_template(get_stub_path("exception"), context)
244
+ output_path = get_project_root() / "app" / "exceptions" / f"{snake_name}.py"
245
+
246
+ write_file(str(output_path), content)
247
+ typer.secho(f"āœ“ Created exception: {output_path}", fg=typer.colors.GREEN)
248
+
249
+
250
+ @app.command("test")
251
+ def make_test(name: str):
252
+ """Generate a new test file."""
253
+ class_name = to_pascal_case(name)
254
+ snake_name = to_snake_case(name)
255
+
256
+ context = {
257
+ "class_name": class_name,
258
+ "snake_name": snake_name,
259
+ "kebab_name": to_kebab_case(name),
260
+ }
261
+
262
+ content = render_template(get_stub_path("test"), context)
263
+ output_path = get_project_root() / "tests" / "unit" / f"test_{snake_name}.py"
264
+
265
+ write_file(str(output_path), content)
266
+ typer.secho(f"āœ“ Created test: {output_path}", fg=typer.colors.GREEN)
@@ -0,0 +1,69 @@
1
+ import subprocess
2
+
3
+ import typer
4
+
5
+ from elric_cli.utils import get_project_root
6
+
7
+ app = typer.Typer(help="Database migration commands")
8
+
9
+
10
+ def run_alembic_command(args: list[str]) -> None:
11
+ """Run an Alembic command."""
12
+ try:
13
+ result = subprocess.run(
14
+ ["alembic"] + args,
15
+ cwd=get_project_root(),
16
+ capture_output=True,
17
+ text=True,
18
+ )
19
+
20
+ if result.returncode == 0:
21
+ typer.echo(result.stdout)
22
+ else:
23
+ typer.secho(f"āœ— Command failed", fg=typer.colors.RED)
24
+ typer.echo(result.stderr)
25
+ raise typer.Exit(1)
26
+ except FileNotFoundError:
27
+ typer.secho("āœ— Alembic not found. Make sure it's installed.", fg=typer.colors.RED)
28
+ raise typer.Exit(1)
29
+
30
+
31
+ @app.command()
32
+ def migrate():
33
+ """Run all pending migrations (alembic upgrade head)."""
34
+ typer.secho("Running migrations...", fg=typer.colors.BLUE)
35
+ run_alembic_command(["upgrade", "head"])
36
+ typer.secho("āœ“ Migrations completed", fg=typer.colors.GREEN)
37
+
38
+
39
+ @app.command("rollback")
40
+ def migrate_rollback():
41
+ """Rollback the last migration (alembic downgrade -1)."""
42
+ typer.secho("Rolling back last migration...", fg=typer.colors.BLUE)
43
+ run_alembic_command(["downgrade", "-1"])
44
+ typer.secho("āœ“ Rollback completed", fg=typer.colors.GREEN)
45
+
46
+
47
+ @app.command("fresh")
48
+ def migrate_fresh():
49
+ """Drop all tables and re-run all migrations."""
50
+ confirm = typer.confirm(
51
+ "āš ļø This will DROP ALL TABLES and re-run migrations. Continue?",
52
+ abort=True,
53
+ )
54
+
55
+ if confirm:
56
+ typer.secho("Dropping all tables...", fg=typer.colors.BLUE)
57
+ run_alembic_command(["downgrade", "base"])
58
+
59
+ typer.secho("Running all migrations...", fg=typer.colors.BLUE)
60
+ run_alembic_command(["upgrade", "head"])
61
+
62
+ typer.secho("āœ“ Fresh migration completed", fg=typer.colors.GREEN)
63
+
64
+
65
+ @app.command("status")
66
+ def migrate_status():
67
+ """Show current migration status (alembic current)."""
68
+ typer.secho("Current migration status:", fg=typer.colors.BLUE)
69
+ run_alembic_command(["current"])
@@ -0,0 +1,72 @@
1
+ import shutil
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ DEFAULT_TEMPLATE_REPO = "https://github.com/marcoinsabato/elric-template.git"
8
+
9
+
10
+ def new_project(
11
+ name: str = typer.Argument(..., help="Project name"),
12
+ directory: Path = typer.Option(Path("."), "--directory", "-d", help="Directory where project will be created"),
13
+ install: bool = typer.Option(True, "--install/--no-install", help="Run `uv sync` after project creation"),
14
+ keep_git: bool = typer.Option(False, "--keep-git", help="Keep template .git history"),
15
+ ):
16
+ """Create a new Elric project from a remote template repository."""
17
+ template_repo = DEFAULT_TEMPLATE_REPO
18
+
19
+ target_path = (directory / name).resolve()
20
+ if target_path.exists() and any(target_path.iterdir()):
21
+ typer.secho(f"āœ— Target directory is not empty: {target_path}", fg=typer.colors.RED)
22
+ raise typer.Exit(1)
23
+ if target_path.exists() and not target_path.is_dir():
24
+ typer.secho(f"āœ— Target path is not a directory: {target_path}", fg=typer.colors.RED)
25
+ raise typer.Exit(1)
26
+
27
+ clone_command = ["git", "clone", "--depth", "1"]
28
+ clone_command.extend([template_repo, str(target_path)])
29
+
30
+ typer.secho(f"Creating project: {name}", fg=typer.colors.BLUE)
31
+ typer.secho(f"Template: {template_repo}", fg=typer.colors.BLUE)
32
+ typer.secho(f"Destination: {target_path}", fg=typer.colors.BLUE)
33
+
34
+ try:
35
+ clone_result = subprocess.run(clone_command, capture_output=True, text=True)
36
+ except FileNotFoundError:
37
+ typer.secho("āœ— Git not found. Install git and retry.", fg=typer.colors.RED)
38
+ raise typer.Exit(1)
39
+
40
+ if clone_result.returncode != 0:
41
+ typer.secho("āœ— Failed to clone template repository.", fg=typer.colors.RED)
42
+ if clone_result.stderr:
43
+ typer.echo(clone_result.stderr.strip())
44
+ raise typer.Exit(1)
45
+
46
+ if not keep_git:
47
+ git_dir = target_path / ".git"
48
+ if git_dir.exists() and git_dir.is_dir():
49
+ shutil.rmtree(git_dir)
50
+
51
+ if install:
52
+ typer.secho("Installing dependencies with uv sync...", fg=typer.colors.BLUE)
53
+ try:
54
+ install_result = subprocess.run(["uv", "sync"], cwd=target_path, capture_output=True, text=True)
55
+ except FileNotFoundError:
56
+ typer.secho("⚠ uv not found. Skip install and run `uv sync` manually.", fg=typer.colors.YELLOW)
57
+ install_result = None
58
+ if install_result and install_result.returncode != 0:
59
+ typer.secho("⚠ Dependency installation failed. You can run `uv sync` manually.", fg=typer.colors.YELLOW)
60
+ if install_result.stderr:
61
+ typer.echo(install_result.stderr.strip())
62
+
63
+ typer.secho("āœ“ Project created successfully!", fg=typer.colors.GREEN)
64
+ typer.echo("")
65
+ typer.secho("Next steps:", fg=typer.colors.BLUE)
66
+ typer.echo(f" cd {target_path}")
67
+ if not install:
68
+ typer.echo(" uv sync")
69
+ typer.echo(" cp .env.example .env")
70
+ typer.echo(" docker compose -f docker/docker-compose.yml up -d db redis")
71
+ typer.echo(" elric migrate")
72
+ typer.echo(" elric serve")
@@ -0,0 +1,49 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ app = typer.Typer(help="Route management commands")
6
+ console = Console()
7
+
8
+
9
+ @app.command("list")
10
+ def list_routes():
11
+ """List all registered routes in the FastAPI application."""
12
+ try:
13
+ from app import create_app
14
+
15
+ app_instance = create_app()
16
+
17
+ table = Table(title="Registered Routes")
18
+ table.add_column("Method", style="cyan")
19
+ table.add_column("Path", style="green")
20
+ table.add_column("Name", style="yellow")
21
+
22
+ routes = []
23
+ for route in app_instance.routes:
24
+ if hasattr(route, "methods"):
25
+ for method in route.methods:
26
+ if method != "HEAD":
27
+ routes.append({
28
+ "method": method,
29
+ "path": route.path,
30
+ "name": route.name or "-",
31
+ })
32
+
33
+ routes.sort(key=lambda x: (x["path"], x["method"]))
34
+
35
+ for route in routes:
36
+ table.add_row(route["method"], route["path"], route["name"])
37
+
38
+ console.print(table)
39
+ console.print(f"\n[bold]Total routes:[/bold] {len(routes)}")
40
+ except ModuleNotFoundError as e:
41
+ typer.secho(
42
+ "āœ— This command must run inside an Elric application project.",
43
+ fg=typer.colors.RED,
44
+ )
45
+ typer.secho(f" Details: {e}", fg=typer.colors.YELLOW)
46
+ raise typer.Exit(1)
47
+ except Exception as e:
48
+ typer.secho(f"āœ— Failed to list routes: {e}", fg=typer.colors.RED)
49
+ raise typer.Exit(1)
@@ -0,0 +1,33 @@
1
+ import subprocess
2
+
3
+ import typer
4
+
5
+ from elric_cli.utils import get_project_root
6
+
7
+
8
+ def serve(
9
+ host: str = typer.Option("0.0.0.0", help="Host to bind to"),
10
+ port: int = typer.Option(8000, help="Port to bind to"),
11
+ reload: bool = typer.Option(True, help="Enable auto-reload"),
12
+ ):
13
+ """Start the development server with uvicorn."""
14
+ typer.secho(f"Starting server on {host}:{port}...", fg=typer.colors.BLUE)
15
+
16
+ cmd = [
17
+ "uvicorn",
18
+ "app:create_app",
19
+ "--factory",
20
+ "--host", host,
21
+ "--port", str(port),
22
+ ]
23
+
24
+ if reload:
25
+ cmd.append("--reload")
26
+
27
+ try:
28
+ subprocess.run(cmd, cwd=get_project_root())
29
+ except KeyboardInterrupt:
30
+ typer.secho("\nāœ“ Server stopped", fg=typer.colors.GREEN)
31
+ except FileNotFoundError:
32
+ typer.secho("āœ— Uvicorn not found. Make sure it's installed.", fg=typer.colors.RED)
33
+ raise typer.Exit(1)