lambda-forge-cli-aws 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.
- lambda_forge_cli_aws-1.0.0.dist-info/METADATA +352 -0
- lambda_forge_cli_aws-1.0.0.dist-info/RECORD +80 -0
- lambda_forge_cli_aws-1.0.0.dist-info/WHEEL +4 -0
- lambda_forge_cli_aws-1.0.0.dist-info/entry_points.txt +2 -0
- lambda_forge_cli_aws-1.0.0.dist-info/licenses/LICENSE +189 -0
- lambdaforge/__init__.py +10 -0
- lambdaforge/bedrock_client.py +109 -0
- lambdaforge/cli.py +428 -0
- lambdaforge/deploy.py +308 -0
- lambdaforge/logs.py +185 -0
- lambdaforge/renderer.py +72 -0
- lambdaforge/spec_generator.py +171 -0
- lambdaforge/status.py +111 -0
- lambdaforge/templates.py +106 -0
- lambdaforge/upgrade.py +231 -0
- templates/python-api/cookiecutter.json +12 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/dependabot.yml +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +90 -0
- templates/python-api/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-api/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-api/{{cookiecutter.project_slug}}/README.md +130 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/app.js +26 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/cdk.json +20 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +155 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-api/{{cookiecutter.project_slug}}/pyproject.toml +66 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements-dev.txt +8 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements.txt +3 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/handler.py +93 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/utils/logger.py +39 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/test_handler.py +68 -0
- templates/python-cron/cookiecutter.json +13 -0
- templates/python-cron/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-cron/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +55 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-cron/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-cron/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-cron/{{cookiecutter.project_slug}}/src/handler.py +53 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/test_handler.py +48 -0
- templates/python-queue/cookiecutter.json +12 -0
- templates/python-queue/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-queue/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +79 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-queue/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-queue/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-queue/{{cookiecutter.project_slug}}/src/handler.py +64 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/test_handler.py +60 -0
- templates/python-s3/cookiecutter.json +13 -0
- templates/python-s3/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-s3/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +89 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-s3/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-s3/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-s3/{{cookiecutter.project_slug}}/src/handler.py +86 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/test_handler.py +71 -0
- templates/python-stream/cookiecutter.json +12 -0
- templates/python-stream/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-stream/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +81 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-stream/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-stream/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-stream/{{cookiecutter.project_slug}}/src/handler.py +69 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/test_handler.py +64 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Bedrock client — wrapper for Amazon Bedrock with retry and streaming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import boto3
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BedrockClient:
|
|
12
|
+
"""Wrapper for Amazon Bedrock with built-in retry and error handling."""
|
|
13
|
+
|
|
14
|
+
DEFAULT_MODEL = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
15
|
+
REGION = "us-east-1"
|
|
16
|
+
|
|
17
|
+
def __init__(self, region: str = REGION, model_id: str = DEFAULT_MODEL) -> None:
|
|
18
|
+
self.client = boto3.client("bedrock-runtime", region_name=region)
|
|
19
|
+
self.region = region
|
|
20
|
+
self.model_id = model_id
|
|
21
|
+
|
|
22
|
+
def generate(
|
|
23
|
+
self,
|
|
24
|
+
system_prompt: str,
|
|
25
|
+
user_message: str,
|
|
26
|
+
max_tokens: int = 2048,
|
|
27
|
+
temperature: float = 0.3,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Generate text with Claude on Bedrock.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
system_prompt: System instructions for Claude
|
|
33
|
+
user_message: The user's request
|
|
34
|
+
max_tokens: Maximum tokens in response
|
|
35
|
+
temperature: Sampling temperature (0.0-1.0)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Generated text from Claude
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
Exception: If the Bedrock call fails
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
response = self.client.converse(
|
|
45
|
+
modelId=self.model_id,
|
|
46
|
+
messages=[
|
|
47
|
+
{
|
|
48
|
+
"role": "user",
|
|
49
|
+
"content": [{"text": user_message}],
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
system=[{"text": system_prompt}],
|
|
53
|
+
inferenceConfig={
|
|
54
|
+
"maxTokens": max_tokens,
|
|
55
|
+
"temperature": temperature,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
return response["output"]["message"]["content"][0]["text"]
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise BedrockError(f"Bedrock call failed: {e}") from e
|
|
62
|
+
|
|
63
|
+
def generate_json(
|
|
64
|
+
self,
|
|
65
|
+
system_prompt: str,
|
|
66
|
+
user_message: str,
|
|
67
|
+
max_tokens: int = 2048,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""Generate JSON with Claude. Strips markdown fences if present."""
|
|
70
|
+
text = self.generate(
|
|
71
|
+
system_prompt=system_prompt,
|
|
72
|
+
user_message=user_message,
|
|
73
|
+
max_tokens=max_tokens,
|
|
74
|
+
temperature=0.2,
|
|
75
|
+
)
|
|
76
|
+
return self._extract_json(text)
|
|
77
|
+
|
|
78
|
+
def _extract_json(self, text: str) -> dict[str, Any]:
|
|
79
|
+
"""Extract JSON from text, handling markdown code fences."""
|
|
80
|
+
text = text.strip()
|
|
81
|
+
|
|
82
|
+
# Strip markdown code fences
|
|
83
|
+
if text.startswith("```"):
|
|
84
|
+
lines = text.split("\n")
|
|
85
|
+
if lines[-1].startswith("```"):
|
|
86
|
+
text = "\n".join(lines[1:-1])
|
|
87
|
+
else:
|
|
88
|
+
text = "\n".join(lines[1:])
|
|
89
|
+
|
|
90
|
+
return json.loads(text)
|
|
91
|
+
|
|
92
|
+
def estimate_cost(
|
|
93
|
+
self,
|
|
94
|
+
input_tokens: int,
|
|
95
|
+
output_tokens: int,
|
|
96
|
+
) -> float:
|
|
97
|
+
"""Estimate cost in USD."""
|
|
98
|
+
# Approximate pricing for Claude 3.5 Sonnet
|
|
99
|
+
PRICES = {
|
|
100
|
+
"input": 0.003 / 1000,
|
|
101
|
+
"output": 0.015 / 1000,
|
|
102
|
+
}
|
|
103
|
+
return input_tokens * PRICES["input"] + output_tokens * PRICES["output"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BedrockError(Exception):
|
|
107
|
+
"""Raised when a Bedrock operation fails."""
|
|
108
|
+
|
|
109
|
+
pass
|
lambdaforge/cli.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""LambdaForge CLI — main entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from lambdaforge import __version__
|
|
14
|
+
from lambdaforge.deploy import bootstrap_account, deploy_lambda, destroy_stack
|
|
15
|
+
from lambdaforge.logs import show_logs
|
|
16
|
+
from lambdaforge.renderer import TemplateRenderer
|
|
17
|
+
from lambdaforge.spec_generator import SpecGenerator
|
|
18
|
+
from lambdaforge.status import show_status
|
|
19
|
+
from lambdaforge.templates import TEMPLATES
|
|
20
|
+
from lambdaforge.upgrade import upgrade_lambda
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
@click.version_option(version=__version__, prog_name="lambdaforge")
|
|
27
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output")
|
|
28
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-essential output")
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def cli(ctx: click.Context, verbose: bool, quiet: bool) -> None:
|
|
31
|
+
"""LambdaForge — Lambda production-ready en 60 segundos."""
|
|
32
|
+
ctx.ensure_object(dict)
|
|
33
|
+
ctx.obj["verbose"] = verbose
|
|
34
|
+
ctx.obj["quiet"] = quiet
|
|
35
|
+
if quiet:
|
|
36
|
+
console.quiet = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command()
|
|
40
|
+
@click.argument("name", required=False, default=None)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--template",
|
|
43
|
+
"-t",
|
|
44
|
+
default=None,
|
|
45
|
+
type=click.Choice(list(TEMPLATES.keys())),
|
|
46
|
+
help="Template to use",
|
|
47
|
+
)
|
|
48
|
+
@click.option("--description", default=None, help="Project description")
|
|
49
|
+
@click.option("--author", default=None, help="Author name")
|
|
50
|
+
@click.option("--email", default=None, help="Author email")
|
|
51
|
+
@click.option("--runtime", default="python3.12", help="Lambda runtime")
|
|
52
|
+
@click.option("--trigger", default="api-gateway", help="Trigger type")
|
|
53
|
+
@click.option("--database", default=None, help="Database (dynamodb)")
|
|
54
|
+
@click.option("--auth", default=None, help="Auth method (cognito, iam)")
|
|
55
|
+
@click.option(
|
|
56
|
+
"--observability",
|
|
57
|
+
"-o",
|
|
58
|
+
default="xray",
|
|
59
|
+
help="Observability (xray, basic)",
|
|
60
|
+
)
|
|
61
|
+
@click.option("--region", default="us-east-1", help="AWS region")
|
|
62
|
+
@click.option("--memory", default=512, help="Memory in MB")
|
|
63
|
+
@click.option("--timeout", default=30, help="Timeout in seconds")
|
|
64
|
+
@click.option(
|
|
65
|
+
"--no-specs",
|
|
66
|
+
is_flag=True,
|
|
67
|
+
help="Skip generating .kiro/specs/ (saves Bedrock cost)",
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--budget",
|
|
71
|
+
default=0.50,
|
|
72
|
+
type=float,
|
|
73
|
+
help="Maximum USD for Bedrock specs generation",
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--model",
|
|
77
|
+
default=None,
|
|
78
|
+
help="Bedrock model ID (default: Claude Sonnet 4.5)",
|
|
79
|
+
)
|
|
80
|
+
@click.option(
|
|
81
|
+
"--output",
|
|
82
|
+
"-d",
|
|
83
|
+
default=".",
|
|
84
|
+
type=click.Path(),
|
|
85
|
+
help="Output directory",
|
|
86
|
+
)
|
|
87
|
+
@click.option("--git/--no-git", default=True, help="Initialize git repo")
|
|
88
|
+
def new(
|
|
89
|
+
name: str | None,
|
|
90
|
+
template: str | None,
|
|
91
|
+
description: str | None,
|
|
92
|
+
author: str | None,
|
|
93
|
+
email: str | None,
|
|
94
|
+
runtime: str,
|
|
95
|
+
trigger: str,
|
|
96
|
+
database: str | None,
|
|
97
|
+
auth: str | None,
|
|
98
|
+
observability: str,
|
|
99
|
+
region: str,
|
|
100
|
+
memory: int,
|
|
101
|
+
timeout: int,
|
|
102
|
+
no_specs: bool,
|
|
103
|
+
budget: float,
|
|
104
|
+
model: str | None,
|
|
105
|
+
output: str,
|
|
106
|
+
git: bool,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Create a new Lambda project.
|
|
109
|
+
|
|
110
|
+
Run without arguments for interactive mode:
|
|
111
|
+
|
|
112
|
+
lambdaforge new
|
|
113
|
+
|
|
114
|
+
Or pass arguments directly:
|
|
115
|
+
|
|
116
|
+
lambdaforge new payment-processor
|
|
117
|
+
|
|
118
|
+
lambdaforge new image-resizer --template python-s3
|
|
119
|
+
|
|
120
|
+
lambdaforge new webhooker --template python-queue --no-specs
|
|
121
|
+
"""
|
|
122
|
+
# Interactive mode when no name is provided
|
|
123
|
+
if name is None:
|
|
124
|
+
import questionary
|
|
125
|
+
|
|
126
|
+
name = questionary.text(
|
|
127
|
+
"Project name:",
|
|
128
|
+
validate=lambda v: len(v) > 0 or "Project name is required",
|
|
129
|
+
).ask()
|
|
130
|
+
if not name:
|
|
131
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
|
|
134
|
+
if template is None:
|
|
135
|
+
template = questionary.select(
|
|
136
|
+
"Template:",
|
|
137
|
+
choices=[
|
|
138
|
+
questionary.Choice(
|
|
139
|
+
title=f"{k} — {v['description']}",
|
|
140
|
+
value=k,
|
|
141
|
+
)
|
|
142
|
+
for k, v in TEMPLATES.items()
|
|
143
|
+
],
|
|
144
|
+
).ask()
|
|
145
|
+
|
|
146
|
+
if description is None:
|
|
147
|
+
description = questionary.text(
|
|
148
|
+
"Description:",
|
|
149
|
+
default=f"Serverless {name} built with AWS Lambda",
|
|
150
|
+
).ask()
|
|
151
|
+
|
|
152
|
+
if author is None:
|
|
153
|
+
author = questionary.text("Author name:", default="LambdaForge User").ask()
|
|
154
|
+
|
|
155
|
+
if email is None:
|
|
156
|
+
email = questionary.text("Author email:", default="user@example.com").ask()
|
|
157
|
+
|
|
158
|
+
region = questionary.text("AWS region:", default=region).ask() or region
|
|
159
|
+
|
|
160
|
+
# Defaults for non-interactive mode
|
|
161
|
+
if template is None:
|
|
162
|
+
template = "python-api"
|
|
163
|
+
if description is None:
|
|
164
|
+
description = f"Serverless {name} built with AWS Lambda"
|
|
165
|
+
if author is None:
|
|
166
|
+
author = "LambdaForge User"
|
|
167
|
+
if email is None:
|
|
168
|
+
email = "user@example.com"
|
|
169
|
+
console.print(
|
|
170
|
+
Panel(
|
|
171
|
+
f"[bold cyan]LambdaForge v{__version__}[/bold cyan]\n"
|
|
172
|
+
f"Creating: [yellow]{name}[/yellow]\n"
|
|
173
|
+
f"Template: [yellow]{template}[/yellow]\n"
|
|
174
|
+
f"Region: [yellow]{region}[/yellow]",
|
|
175
|
+
title="[bold]🔥 LambdaForge[/bold]",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
output_path = Path(output) / name
|
|
180
|
+
if output_path.exists():
|
|
181
|
+
if not click.confirm(f"Directory {output_path} exists. Continue?", default=False):
|
|
182
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
183
|
+
sys.exit(0)
|
|
184
|
+
|
|
185
|
+
# Build context for template rendering
|
|
186
|
+
context = {
|
|
187
|
+
"project_name": name,
|
|
188
|
+
"project_name_snake": name.replace("-", "_"),
|
|
189
|
+
"module_name": name.replace("-", "_"),
|
|
190
|
+
"description": description,
|
|
191
|
+
"author_name": author,
|
|
192
|
+
"author_email": email,
|
|
193
|
+
"runtime": runtime,
|
|
194
|
+
"trigger": trigger,
|
|
195
|
+
"database": database or "none",
|
|
196
|
+
"auth": auth or "none",
|
|
197
|
+
"observability": observability,
|
|
198
|
+
"aws_region": region,
|
|
199
|
+
"memory_mb": memory,
|
|
200
|
+
"timeout_seconds": timeout,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# 1. Render template
|
|
204
|
+
console.print("[dim]1/4 Rendering template...[/dim]")
|
|
205
|
+
renderer = TemplateRenderer()
|
|
206
|
+
try:
|
|
207
|
+
project_path = renderer.render(
|
|
208
|
+
template_name=template,
|
|
209
|
+
project_name=name,
|
|
210
|
+
context=context,
|
|
211
|
+
output_dir=output,
|
|
212
|
+
)
|
|
213
|
+
except FileNotFoundError:
|
|
214
|
+
console.print(f"[red]✗ Template '{template}' not found.[/red]")
|
|
215
|
+
console.print("[yellow]Run `lambdaforge templates` to see available options.[/yellow]")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
console.print(f"[green]✓ Template rendered: {project_path}[/green]")
|
|
218
|
+
|
|
219
|
+
# 2. Generate .kiro/specs/ (if not skipped)
|
|
220
|
+
if not no_specs:
|
|
221
|
+
console.print("[dim]2/4 Generating .kiro/specs/ via Bedrock...[/dim]")
|
|
222
|
+
try:
|
|
223
|
+
spec_gen = SpecGenerator(budget_usd=budget, model_id=model)
|
|
224
|
+
spec_gen.generate_all(project_path, template, context)
|
|
225
|
+
console.print(f"[green]✓ Specs generated[/green] (cost: ${spec_gen.total_cost:.3f})")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
console.print(f"[yellow]⚠ Specs generation failed: {e}[/yellow]")
|
|
228
|
+
console.print("[yellow]Continuing without specs...[/yellow]")
|
|
229
|
+
else:
|
|
230
|
+
console.print("[dim]2/4 Skipping specs (--no-specs)[/dim]")
|
|
231
|
+
|
|
232
|
+
# 3. Initialize git
|
|
233
|
+
if git:
|
|
234
|
+
console.print("[dim]3/4 Initializing git...[/dim]")
|
|
235
|
+
try:
|
|
236
|
+
import git as gitpython
|
|
237
|
+
|
|
238
|
+
repo = gitpython.Repo.init(project_path)
|
|
239
|
+
repo.index.add(
|
|
240
|
+
[str(p.relative_to(project_path)) for p in project_path.rglob("*") if p.is_file()]
|
|
241
|
+
)
|
|
242
|
+
repo.index.commit("Initial Lambda scaffold from LambdaForge")
|
|
243
|
+
console.print("[green]✓ Git initialized[/green]")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
console.print(f"[yellow]⚠ Git init failed: {e}[/yellow]")
|
|
246
|
+
else:
|
|
247
|
+
console.print("[dim]3/4 Skipping git (--no-git)[/dim]")
|
|
248
|
+
|
|
249
|
+
# 4. Done
|
|
250
|
+
console.print(
|
|
251
|
+
Panel(
|
|
252
|
+
f"[bold green]✓ Lambda '{name}' created in {project_path}[/bold green]\n\n"
|
|
253
|
+
f"Next steps:\n"
|
|
254
|
+
f" 1. cd {name}\n"
|
|
255
|
+
f" 2. pip install -r requirements.txt\n"
|
|
256
|
+
f" 3. pytest\n"
|
|
257
|
+
f" 4. cdk deploy",
|
|
258
|
+
title="[bold]Done[/bold]",
|
|
259
|
+
border_style="green",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@cli.command()
|
|
265
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
266
|
+
def upgrade(path: str) -> None:
|
|
267
|
+
"""Upgrade an existing Lambda with LambdaForge best practices.
|
|
268
|
+
|
|
269
|
+
PATH defaults to the current directory if not specified.
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
|
|
273
|
+
lambdaforge upgrade
|
|
274
|
+
|
|
275
|
+
lambdaforge upgrade ./my-existing-lambda
|
|
276
|
+
"""
|
|
277
|
+
upgrade_lambda(Path(path))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@cli.command()
|
|
281
|
+
def templates() -> None:
|
|
282
|
+
"""List available templates."""
|
|
283
|
+
table = Table(title="Available Lambda Templates", show_header=True, header_style="bold cyan")
|
|
284
|
+
table.add_column("Template", style="yellow")
|
|
285
|
+
table.add_column("Description")
|
|
286
|
+
table.add_column("Runtime", style="green")
|
|
287
|
+
table.add_column("Triggers")
|
|
288
|
+
|
|
289
|
+
for name, info in TEMPLATES.items():
|
|
290
|
+
table.add_row(
|
|
291
|
+
name,
|
|
292
|
+
info["description"],
|
|
293
|
+
info.get("runtime", "any"),
|
|
294
|
+
", ".join(info.get("triggers", [])),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
console.print(table)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@cli.command()
|
|
301
|
+
@click.option("--stack", "-s", default=None, help="CDK stack name to deploy")
|
|
302
|
+
@click.option(
|
|
303
|
+
"--env",
|
|
304
|
+
"-e",
|
|
305
|
+
"environment",
|
|
306
|
+
default="dev",
|
|
307
|
+
type=click.Choice(["dev", "staging", "prod"]),
|
|
308
|
+
help="Target environment",
|
|
309
|
+
)
|
|
310
|
+
def deploy(stack: str | None, environment: str) -> None:
|
|
311
|
+
"""Deploy the current Lambda to AWS.
|
|
312
|
+
|
|
313
|
+
Bundles dependencies, installs CDK packages, and runs cdk deploy.
|
|
314
|
+
Use --env to target different environments with different configs.
|
|
315
|
+
|
|
316
|
+
Examples:
|
|
317
|
+
|
|
318
|
+
lambdaforge deploy
|
|
319
|
+
|
|
320
|
+
lambdaforge deploy --env staging
|
|
321
|
+
|
|
322
|
+
lambdaforge deploy --env prod --stack my-api-prod
|
|
323
|
+
"""
|
|
324
|
+
deploy_lambda(Path.cwd(), stack_name=stack, env=environment)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@cli.command()
|
|
328
|
+
@click.option("--region", "-r", default="us-east-1", help="AWS region to bootstrap")
|
|
329
|
+
def bootstrap(region: str) -> None:
|
|
330
|
+
"""Bootstrap CDK in your AWS account (one-time setup).
|
|
331
|
+
|
|
332
|
+
Creates the S3 bucket and IAM roles that CDK needs to deploy stacks.
|
|
333
|
+
Only needs to run once per account/region.
|
|
334
|
+
|
|
335
|
+
Examples:
|
|
336
|
+
|
|
337
|
+
lambdaforge bootstrap
|
|
338
|
+
|
|
339
|
+
lambdaforge bootstrap --region us-west-2
|
|
340
|
+
"""
|
|
341
|
+
bootstrap_account(region=region)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@cli.command()
|
|
345
|
+
@click.option("--stack", "-s", default=None, help="CDK stack name to destroy")
|
|
346
|
+
@click.option(
|
|
347
|
+
"--env",
|
|
348
|
+
"-e",
|
|
349
|
+
"environment",
|
|
350
|
+
default="dev",
|
|
351
|
+
type=click.Choice(["dev", "staging", "prod"]),
|
|
352
|
+
help="Target environment to destroy",
|
|
353
|
+
)
|
|
354
|
+
def destroy(stack: str | None, environment: str) -> None:
|
|
355
|
+
"""Destroy the deployed Lambda stack from AWS.
|
|
356
|
+
|
|
357
|
+
Removes all resources created by `lambdaforge deploy`.
|
|
358
|
+
|
|
359
|
+
Examples:
|
|
360
|
+
|
|
361
|
+
lambdaforge destroy
|
|
362
|
+
|
|
363
|
+
lambdaforge destroy --env staging
|
|
364
|
+
|
|
365
|
+
lambdaforge destroy --env prod
|
|
366
|
+
"""
|
|
367
|
+
destroy_stack(Path.cwd(), stack_name=stack, env=environment)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@cli.command()
|
|
371
|
+
def version() -> None:
|
|
372
|
+
"""Show LambdaForge version."""
|
|
373
|
+
console.print(f"lambdaforge {__version__}")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@cli.command()
|
|
377
|
+
@click.option(
|
|
378
|
+
"--env",
|
|
379
|
+
"-e",
|
|
380
|
+
"environment",
|
|
381
|
+
default="dev",
|
|
382
|
+
type=click.Choice(["dev", "staging", "prod"]),
|
|
383
|
+
help="Environment to check",
|
|
384
|
+
)
|
|
385
|
+
def status(environment: str) -> None:
|
|
386
|
+
"""Show deployment status of the current Lambda project.
|
|
387
|
+
|
|
388
|
+
Queries CloudFormation to display stack status, outputs, and tags.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
|
|
392
|
+
lambdaforge status
|
|
393
|
+
|
|
394
|
+
lambdaforge status --env prod
|
|
395
|
+
"""
|
|
396
|
+
show_status(Path.cwd(), env=environment)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@cli.command()
|
|
400
|
+
@click.option(
|
|
401
|
+
"--env",
|
|
402
|
+
"-e",
|
|
403
|
+
"environment",
|
|
404
|
+
default="dev",
|
|
405
|
+
type=click.Choice(["dev", "staging", "prod"]),
|
|
406
|
+
help="Environment to fetch logs from",
|
|
407
|
+
)
|
|
408
|
+
@click.option("--lines", "-n", default=50, help="Number of log lines to show")
|
|
409
|
+
@click.option("--errors", is_flag=True, help="Show only error logs")
|
|
410
|
+
def logs(environment: str, lines: int, errors: bool) -> None:
|
|
411
|
+
"""Show CloudWatch logs for the deployed Lambda.
|
|
412
|
+
|
|
413
|
+
Fetches recent log events and formats them with color coding.
|
|
414
|
+
Supports structured JSON logs from aws-lambda-powertools.
|
|
415
|
+
|
|
416
|
+
Examples:
|
|
417
|
+
|
|
418
|
+
lambdaforge logs
|
|
419
|
+
|
|
420
|
+
lambdaforge logs --env prod --lines 100
|
|
421
|
+
|
|
422
|
+
lambdaforge logs --errors
|
|
423
|
+
"""
|
|
424
|
+
show_logs(Path.cwd(), env=environment, lines=lines, errors_only=errors)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
cli()
|