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.
Files changed (80) hide show
  1. lambda_forge_cli_aws-1.0.0.dist-info/METADATA +352 -0
  2. lambda_forge_cli_aws-1.0.0.dist-info/RECORD +80 -0
  3. lambda_forge_cli_aws-1.0.0.dist-info/WHEEL +4 -0
  4. lambda_forge_cli_aws-1.0.0.dist-info/entry_points.txt +2 -0
  5. lambda_forge_cli_aws-1.0.0.dist-info/licenses/LICENSE +189 -0
  6. lambdaforge/__init__.py +10 -0
  7. lambdaforge/bedrock_client.py +109 -0
  8. lambdaforge/cli.py +428 -0
  9. lambdaforge/deploy.py +308 -0
  10. lambdaforge/logs.py +185 -0
  11. lambdaforge/renderer.py +72 -0
  12. lambdaforge/spec_generator.py +171 -0
  13. lambdaforge/status.py +111 -0
  14. lambdaforge/templates.py +106 -0
  15. lambdaforge/upgrade.py +231 -0
  16. templates/python-api/cookiecutter.json +12 -0
  17. templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/dependabot.yml +22 -0
  18. templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +90 -0
  19. templates/python-api/{{cookiecutter.project_slug}}/.gitignore +65 -0
  20. templates/python-api/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  21. templates/python-api/{{cookiecutter.project_slug}}/README.md +130 -0
  22. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/app.js +26 -0
  23. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/cdk.json +20 -0
  24. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +155 -0
  25. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  26. templates/python-api/{{cookiecutter.project_slug}}/pyproject.toml +66 -0
  27. templates/python-api/{{cookiecutter.project_slug}}/requirements-dev.txt +8 -0
  28. templates/python-api/{{cookiecutter.project_slug}}/requirements.txt +3 -0
  29. templates/python-api/{{cookiecutter.project_slug}}/src/handler.py +93 -0
  30. templates/python-api/{{cookiecutter.project_slug}}/src/utils/logger.py +39 -0
  31. templates/python-api/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  32. templates/python-api/{{cookiecutter.project_slug}}/tests/test_handler.py +68 -0
  33. templates/python-cron/cookiecutter.json +13 -0
  34. templates/python-cron/{{cookiecutter.project_slug}}/.gitignore +65 -0
  35. templates/python-cron/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  36. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  37. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  38. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +55 -0
  39. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  40. templates/python-cron/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  41. templates/python-cron/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  42. templates/python-cron/{{cookiecutter.project_slug}}/src/handler.py +53 -0
  43. templates/python-cron/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  44. templates/python-cron/{{cookiecutter.project_slug}}/tests/test_handler.py +48 -0
  45. templates/python-queue/cookiecutter.json +12 -0
  46. templates/python-queue/{{cookiecutter.project_slug}}/.gitignore +65 -0
  47. templates/python-queue/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  48. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  49. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  50. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +79 -0
  51. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  52. templates/python-queue/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  53. templates/python-queue/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  54. templates/python-queue/{{cookiecutter.project_slug}}/src/handler.py +64 -0
  55. templates/python-queue/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  56. templates/python-queue/{{cookiecutter.project_slug}}/tests/test_handler.py +60 -0
  57. templates/python-s3/cookiecutter.json +13 -0
  58. templates/python-s3/{{cookiecutter.project_slug}}/.gitignore +65 -0
  59. templates/python-s3/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  60. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  61. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  62. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +89 -0
  63. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  64. templates/python-s3/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  65. templates/python-s3/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  66. templates/python-s3/{{cookiecutter.project_slug}}/src/handler.py +86 -0
  67. templates/python-s3/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  68. templates/python-s3/{{cookiecutter.project_slug}}/tests/test_handler.py +71 -0
  69. templates/python-stream/cookiecutter.json +12 -0
  70. templates/python-stream/{{cookiecutter.project_slug}}/.gitignore +65 -0
  71. templates/python-stream/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  72. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  73. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  74. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +81 -0
  75. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  76. templates/python-stream/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  77. templates/python-stream/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  78. templates/python-stream/{{cookiecutter.project_slug}}/src/handler.py +69 -0
  79. templates/python-stream/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  80. templates/python-stream/{{cookiecutter.project_slug}}/tests/test_handler.py +64 -0
@@ -0,0 +1,171 @@
1
+ """Spec generator — creates .kiro/specs/ files using Amazon Bedrock."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from lambdaforge.bedrock_client import BedrockClient
9
+
10
+
11
+ class SpecGenerator:
12
+ """Generates Kiro-compatible spec files for Lambda projects.
13
+
14
+ Uses Amazon Bedrock with Claude Sonnet 4.5 to generate
15
+ context-aware specs based on the template configuration.
16
+ """
17
+
18
+ SYSTEM_PROMPT = """Eres un AWS Solutions Architect documentando una Lambda recién creada.
19
+
20
+ Recibes la configuración de la Lambda (runtime, triggers, databases, auth).
21
+ Tu trabajo es generar 4 archivos markdown en español que Kiro pueda usar como specs.
22
+
23
+ Output esperado (4 secciones separadas por '---SECTION---'):
24
+
25
+ SECTION 1: overview.md
26
+ - Título H1 con nombre del proyecto
27
+ - 1 párrafo describiendo QUÉ hace
28
+ - 1 párrafo describiendo POR QUÉ existe
29
+ - Lista de "Acceptance Criteria" iniciales
30
+
31
+ SECTION 2: api-design.md
32
+ - Solo si trigger es api-gateway
33
+ - Endpoints REST con method, path, description
34
+ - Request schema (JSON example)
35
+ - Response schema (JSON example)
36
+ - Status codes
37
+
38
+ SECTION 3: data-model.md
39
+ - Solo si database es dynamodb
40
+ - Tabla principal con partition key, sort key
41
+ - GSI/LSI si aplica
42
+ - Access patterns (cómo se consulta)
43
+
44
+ SECTION 4: acceptance-criteria.md
45
+ - 5-10 criterios testeable
46
+ - Formato: - [ ] descripción binaria (pass/fail)
47
+ - Cubren: happy path, errores, validaciones, performance
48
+
49
+ Reglas críticas:
50
+ - Escribe TODO en español
51
+ - Sé específico y accionable
52
+ - NO inventes endpoints que no estén en el template
53
+ - Acceptance criteria deben ser BINARIOS
54
+ - Usa formato markdown limpio (sin HTML)
55
+ - NO incluyas '```markdown' ni otros code fences (esos se añaden al escribir)"""
56
+
57
+ def __init__(
58
+ self,
59
+ region: str = "us-east-1",
60
+ budget_usd: float = 0.50,
61
+ model_id: str | None = None,
62
+ ) -> None:
63
+ self.client = BedrockClient(region=region, model_id=model_id or BedrockClient.DEFAULT_MODEL)
64
+ self.budget_usd = budget_usd
65
+ self.total_cost = 0.0
66
+
67
+ def generate_all(
68
+ self,
69
+ project_path: Path,
70
+ template_name: str,
71
+ context: dict,
72
+ ) -> dict[str, str]:
73
+ """Generate all spec files for a Lambda project.
74
+
75
+ Args:
76
+ project_path: Path to the generated project
77
+ template_name: Template used
78
+ context: Template context variables
79
+
80
+ Returns:
81
+ Dict of {filename: content}
82
+ """
83
+ specs_dir = project_path / ".kiro" / "specs"
84
+ specs_dir.mkdir(parents=True, exist_ok=True)
85
+
86
+ # Build user message
87
+ user_message = f"""Genera las specs para esta Lambda:
88
+
89
+ Nombre: {context.get("project_name")}
90
+ Template: {template_name}
91
+ Runtime: {context.get("runtime", "python3.12")}
92
+ Trigger: {context.get("trigger", "api-gateway")}
93
+ Database: {context.get("database", "none")}
94
+ Auth: {context.get("auth", "none")}
95
+ Observability: {context.get("observability", "xray")}
96
+ Region: {context.get("aws_region", "us-east-1")}
97
+ Memory: {context.get("memory_mb", 512)}MB
98
+ Timeout: {context.get("timeout_seconds", 30)}s
99
+
100
+ Genera las 4 secciones separadas por '---SECTION---'."""
101
+
102
+ # Check budget
103
+ if self.total_cost >= self.budget_usd:
104
+ return {"error": "Budget exhausted, skipping specs generation"}
105
+
106
+ # Call Bedrock
107
+ try:
108
+ response = self.client.generate(
109
+ system_prompt=self.SYSTEM_PROMPT,
110
+ user_message=user_message,
111
+ max_tokens=3000,
112
+ temperature=0.4,
113
+ )
114
+ # Rough cost estimate
115
+ self.total_cost += 0.05 # ~5 cents per call typical
116
+
117
+ except Exception as e:
118
+ return {"error": str(e)}
119
+
120
+ # Parse sections
121
+ sections = self._parse_sections(response)
122
+
123
+ # Write files
124
+ written: dict[str, str] = {}
125
+ for filename, content in sections.items():
126
+ if content and self._is_relevant(filename, context):
127
+ filepath = specs_dir / filename
128
+ filepath.write_text(content, encoding="utf-8")
129
+ written[filename] = content
130
+
131
+ return written
132
+
133
+ def _parse_sections(self, response: str) -> dict[str, str]:
134
+ """Parse the response into sections."""
135
+ sections = {
136
+ "overview.md": "",
137
+ "api-design.md": "",
138
+ "data-model.md": "",
139
+ "acceptance-criteria.md": "",
140
+ }
141
+
142
+ # Split by SECTION marker (supports: ---SECTION---, ---SECTION 1:, etc.)
143
+ parts = re.split(r"---\s*SECTION\s*\d*\s*[:.]?\s*---?\s*", response)
144
+
145
+ if len(parts) >= 5: # 4 sections + preamble
146
+ sections["overview.md"] = parts[1].strip()
147
+ sections["api-design.md"] = parts[2].strip()
148
+ sections["data-model.md"] = parts[3].strip()
149
+ sections["acceptance-criteria.md"] = parts[4].strip()
150
+ elif len(parts) >= 4:
151
+ sections["overview.md"] = parts[1].strip()
152
+ sections["api-design.md"] = parts[2].strip()
153
+ sections["data-model.md"] = parts[3].strip()
154
+ elif len(parts) >= 3:
155
+ sections["overview.md"] = parts[1].strip()
156
+ sections["api-design.md"] = parts[2].strip()
157
+ elif len(parts) >= 2:
158
+ sections["overview.md"] = parts[1].strip()
159
+ else:
160
+ # No markers found — treat entire response as overview
161
+ sections["overview.md"] = response.strip()
162
+
163
+ return sections
164
+
165
+ def _is_relevant(self, filename: str, context: dict) -> bool:
166
+ """Check if a spec file is relevant to the configuration."""
167
+ if filename == "api-design.md":
168
+ return context.get("trigger") == "api-gateway"
169
+ if filename == "data-model.md":
170
+ return context.get("database") == "dynamodb"
171
+ return True
lambdaforge/status.py ADDED
@@ -0,0 +1,111 @@
1
+ """Status command — shows the current deployment state of a Lambda project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+
13
+ console = Console()
14
+
15
+
16
+ def show_status(project_path: Path, env: str = "dev") -> None:
17
+ """Show the deployment status of a Lambda project.
18
+
19
+ Queries CloudFormation for the stack status and outputs.
20
+
21
+ Args:
22
+ project_path: Path to the Lambda project
23
+ env: Target environment
24
+ """
25
+ project_name = project_path.name.replace("_", "-")
26
+ stack_name = f"{project_name}-{env}"
27
+
28
+ stack_info = _get_stack_info(stack_name)
29
+
30
+ if stack_info is None:
31
+ console.print(
32
+ Panel(
33
+ f"[yellow]Stack '{stack_name}' not found.[/yellow]\n\n"
34
+ f"Not deployed to '{env}' yet.\n"
35
+ f"Run: [bold]lambdaforge deploy --env {env}[/bold]",
36
+ title=f"[bold]{project_name}[/bold]",
37
+ border_style="yellow",
38
+ )
39
+ )
40
+ return
41
+
42
+ status = stack_info.get("StackStatus", "UNKNOWN")
43
+ status_color = _status_color(status)
44
+
45
+ outputs = stack_info.get("Outputs", [])
46
+ outputs_dict = {o["OutputKey"]: o["OutputValue"] for o in outputs}
47
+
48
+ lines = [
49
+ f"[bold]Stack:[/bold] {stack_name}",
50
+ f"[bold]Status:[/bold] [{status_color}]{status}[/{status_color}]",
51
+ f"[bold]Environment:[/bold] {env}",
52
+ ]
53
+
54
+ if "Description" in stack_info:
55
+ lines.append(f"[bold]Description:[/bold] {stack_info['Description']}")
56
+
57
+ updated = stack_info.get("LastUpdatedTime", stack_info.get("CreationTime", ""))
58
+ if updated:
59
+ lines.append(f"[bold]Last deployed:[/bold] {updated}")
60
+
61
+ if outputs_dict:
62
+ lines.append("")
63
+ lines.append("[bold]Outputs:[/bold]")
64
+ for key, value in outputs_dict.items():
65
+ lines.append(f" {key}: [cyan]{value}[/cyan]")
66
+
67
+ tags = stack_info.get("Tags", [])
68
+ if tags:
69
+ lines.append("")
70
+ lines.append("[bold]Tags:[/bold]")
71
+ for tag in tags:
72
+ lines.append(f" {tag['Key']}: {tag['Value']}")
73
+
74
+ console.print(
75
+ Panel("\n".join(lines), title=f"[bold]{project_name}[/bold]", border_style=status_color)
76
+ )
77
+
78
+
79
+ def _get_stack_info(stack_name: str) -> dict[str, Any] | None:
80
+ """Query CloudFormation for stack information."""
81
+ try:
82
+ result = subprocess.run(
83
+ [
84
+ "aws",
85
+ "cloudformation",
86
+ "describe-stacks",
87
+ "--stack-name",
88
+ stack_name,
89
+ "--output",
90
+ "json",
91
+ ],
92
+ capture_output=True,
93
+ text=True,
94
+ check=True,
95
+ )
96
+ data = json.loads(result.stdout)
97
+ stacks = data.get("Stacks", [])
98
+ return stacks[0] if stacks else None
99
+ except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
100
+ return None
101
+
102
+
103
+ def _status_color(status: str) -> str:
104
+ """Map CloudFormation status to a Rich color."""
105
+ if "COMPLETE" in status and "DELETE" not in status:
106
+ return "green"
107
+ elif "IN_PROGRESS" in status:
108
+ return "yellow"
109
+ elif "FAILED" in status or "DELETE" in status:
110
+ return "red"
111
+ return "dim"
@@ -0,0 +1,106 @@
1
+ """Template catalog — defines all available Lambda templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ TEMPLATES: dict[str, dict[str, Any]] = {
8
+ "python-api": {
9
+ "description": "Lambda triggered by API Gateway with Python 3.12",
10
+ "runtime": "python3.12",
11
+ "triggers": ["api-gateway", "alb", "function-url"],
12
+ "features": [
13
+ "cors",
14
+ "auth-cognito",
15
+ "request-validation",
16
+ "error-handler",
17
+ "structured-logging",
18
+ "xray-tracing",
19
+ ],
20
+ "dependencies": [
21
+ "pydantic>=2.5.0",
22
+ "aws-lambda-powertools>=2.30.0",
23
+ "aws-xray-sdk>=2.12.0",
24
+ ],
25
+ "default_memory": 512,
26
+ "default_timeout": 30,
27
+ },
28
+ "python-cron": {
29
+ "description": "Lambda triggered by EventBridge schedule (cron job)",
30
+ "runtime": "python3.12",
31
+ "triggers": ["eventbridge-schedule"],
32
+ "features": [
33
+ "retry-config",
34
+ "dlq",
35
+ "structured-logging",
36
+ "xray-tracing",
37
+ "timeout-config",
38
+ ],
39
+ "dependencies": [
40
+ "aws-lambda-powertools>=2.30.0",
41
+ "boto3>=1.34.0",
42
+ ],
43
+ "default_memory": 512,
44
+ "default_timeout": 300,
45
+ },
46
+ "python-queue": {
47
+ "description": "Lambda triggered by SQS queue (with DLQ + batch)",
48
+ "runtime": "python3.12",
49
+ "triggers": ["sqs", "sns"],
50
+ "features": [
51
+ "batch-processing",
52
+ "partial-batch-response",
53
+ "dlq",
54
+ "idempotency",
55
+ "xray-tracing",
56
+ ],
57
+ "dependencies": [
58
+ "aws-lambda-powertools>=2.30.0",
59
+ ],
60
+ "default_memory": 1024,
61
+ "default_timeout": 60,
62
+ },
63
+ "python-stream": {
64
+ "description": "Lambda triggered by Kinesis or DynamoDB Streams",
65
+ "runtime": "python3.12",
66
+ "triggers": ["kinesis", "dynamodb-streams"],
67
+ "features": [
68
+ "checkpointing",
69
+ "iterator-age-alarm",
70
+ "dlq",
71
+ "xray-tracing",
72
+ ],
73
+ "dependencies": [
74
+ "aws-lambda-powertools>=2.30.0",
75
+ ],
76
+ "default_memory": 1024,
77
+ "default_timeout": 60,
78
+ },
79
+ "python-s3": {
80
+ "description": "Lambda triggered by S3 events (file processing)",
81
+ "runtime": "python3.12",
82
+ "triggers": ["s3"],
83
+ "features": [
84
+ "event-filtering",
85
+ "large-file-handling",
86
+ "multipart-download",
87
+ "xray-tracing",
88
+ ],
89
+ "dependencies": [
90
+ "boto3>=1.34.0",
91
+ "smart-open>=6.4.0",
92
+ ],
93
+ "default_memory": 1024,
94
+ "default_timeout": 120,
95
+ },
96
+ }
97
+
98
+
99
+ def get_template(name: str) -> dict[str, Any] | None:
100
+ """Get a template by name."""
101
+ return TEMPLATES.get(name)
102
+
103
+
104
+ def list_template_names() -> list[str]:
105
+ """List all available template names."""
106
+ return list(TEMPLATES.keys())
lambdaforge/upgrade.py ADDED
@@ -0,0 +1,231 @@
1
+ """Upgrade mode — adds LambdaForge best practices to existing Lambdas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from lambdaforge.spec_generator import SpecGenerator
13
+
14
+ console = Console()
15
+
16
+
17
+ def upgrade_lambda(project_path: Path) -> None:
18
+ """Upgrade an existing Lambda with LambdaForge best practices.
19
+
20
+ Args:
21
+ project_path: Path to the Lambda project
22
+ """
23
+ console.print(
24
+ Panel(
25
+ f"[bold cyan]Upgrading: {project_path}[/bold cyan]",
26
+ title="[bold]🔥 LambdaForge Upgrade[/bold]",
27
+ )
28
+ )
29
+
30
+ # 1. Create backup
31
+ backup_dir = project_path / ".lambdaforge-backup" / datetime.now().strftime("%Y%m%d-%H%M%S")
32
+ console.print(f"[dim]Creating backup at {backup_dir}...[/dim]")
33
+ _backup_project(project_path, backup_dir)
34
+
35
+ # 2. Detect what's missing
36
+ missing = _detect_missing(project_path)
37
+ console.print(f"[dim]Detected missing features: {', '.join(missing)}[/dim]")
38
+
39
+ # 3. Add what's missing
40
+ if "xray" in missing:
41
+ console.print("[dim]Adding X-Ray tracing...[/dim]")
42
+ _add_xray(project_path)
43
+
44
+ if "structured-logging" in missing:
45
+ console.print("[dim]Adding structured logging...[/dim]")
46
+ _add_structured_logging(project_path)
47
+
48
+ if "tests" in missing:
49
+ console.print("[dim]Adding basic tests...[/dim]")
50
+ _add_basic_tests(project_path)
51
+
52
+ if ".kiro/specs/" in missing or "specs" in missing:
53
+ console.print("[dim]Generating .kiro/specs/ via Bedrock...[/dim]")
54
+ try:
55
+ spec_gen = SpecGenerator()
56
+ spec_gen.generate_all(
57
+ project_path=project_path,
58
+ template_name="upgraded",
59
+ context={
60
+ "project_name": project_path.name,
61
+ "runtime": "python3.12",
62
+ "trigger": "unknown",
63
+ "database": "none",
64
+ "auth": "none",
65
+ "observability": "xray",
66
+ "aws_region": "us-east-1",
67
+ },
68
+ )
69
+ console.print("[green]✓ Specs generated[/green]")
70
+ except Exception as e:
71
+ console.print(f"[yellow]⚠ Specs generation failed: {e}[/yellow]")
72
+
73
+ # 4. Add CI/CD if missing
74
+ if "github-actions" in missing:
75
+ console.print("[dim]Adding GitHub Actions...[/dim]")
76
+ _add_github_actions(project_path)
77
+
78
+ console.print(
79
+ Panel(
80
+ f"[bold green]✓ Upgrade complete[/bold green]\n\n"
81
+ f"Backup: {backup_dir}\n"
82
+ f"Review changes with: git diff",
83
+ title="[bold]Done[/bold]",
84
+ border_style="green",
85
+ )
86
+ )
87
+
88
+
89
+ def _backup_project(project_path: Path, backup_dir: Path) -> None:
90
+ """Create a backup of the project before upgrade."""
91
+ backup_dir.mkdir(parents=True, exist_ok=True)
92
+ for item in project_path.iterdir():
93
+ if item.name.startswith(".lambdaforge-backup"):
94
+ continue
95
+ if item.is_file():
96
+ shutil.copy2(item, backup_dir / item.name)
97
+ elif item.is_dir():
98
+ shutil.copytree(item, backup_dir / item.name, dirs_exist_ok=True)
99
+
100
+
101
+ def _detect_missing(project_path: Path) -> list[str]:
102
+ """Detect what features are missing from the project."""
103
+ missing = []
104
+
105
+ # Check X-Ray
106
+ handler_files = list(project_path.glob("**/handler.py")) + list(
107
+ project_path.glob("**/lambda_function.py")
108
+ )
109
+ has_xray = any("@xray_recorder" in f.read_text() for f in handler_files if f.exists())
110
+ if not has_xray:
111
+ missing.append("xray")
112
+
113
+ # Check structured logging
114
+ has_logging = any("logger" in f.name.lower() for f in project_path.rglob("*.py"))
115
+ if not has_logging:
116
+ missing.append("structured-logging")
117
+
118
+ # Check tests
119
+ has_tests = (project_path / "tests").exists() and any(project_path.glob("tests/test_*.py"))
120
+ if not has_tests:
121
+ missing.append("tests")
122
+
123
+ # Check .kiro/specs/
124
+ specs_dir = project_path / ".kiro" / "specs"
125
+ if not specs_dir.exists() or not any(specs_dir.glob("*.md")):
126
+ missing.append(".kiro/specs/")
127
+
128
+ # Check GitHub Actions
129
+ gh_actions = project_path / ".github" / "workflows"
130
+ if not gh_actions.exists() or not any(gh_actions.glob("*.yml")):
131
+ missing.append("github-actions")
132
+
133
+ return missing
134
+
135
+
136
+ def _add_xray(project_path: Path) -> None:
137
+ """Add X-Ray tracing to handler files."""
138
+ # Add to requirements.txt
139
+ req_file = project_path / "requirements.txt"
140
+ if req_file.exists():
141
+ content = req_file.read_text()
142
+ if "aws-xray-sdk" not in content:
143
+ with req_file.open("a") as f:
144
+ f.write("\n# LambdaForge: X-Ray tracing\naws-xray-sdk>=2.12.0\n")
145
+
146
+ # Add decorator to handler
147
+ for handler in project_path.glob("**/handler.py"):
148
+ if handler.exists():
149
+ content = handler.read_text()
150
+ if "@xray_recorder.capture" not in content:
151
+ # Add import at top
152
+ content = "from aws_xray_sdk.core import xray_recorder\n" + content
153
+ handler.write_text(content)
154
+
155
+
156
+ def _add_structured_logging(project_path: Path) -> None:
157
+ """Add a basic logger module."""
158
+ utils_dir = project_path / "src" / "utils"
159
+ if not utils_dir.exists():
160
+ utils_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ logger_file = utils_dir / "logger.py"
163
+ if not logger_file.exists():
164
+ logger_file.write_text(
165
+ '"""Structured logger for the Lambda."""\n'
166
+ "import logging\n"
167
+ "import json\n\n\n"
168
+ "logger = logging.getLogger()\n"
169
+ "logger.setLevel(logging.INFO)\n\n\n"
170
+ "def log_event(event_type: str, **kwargs):\n"
171
+ ' """Log a structured event."""\n'
172
+ ' payload = {"type": event_type, **kwargs}\n'
173
+ " logger.info(json.dumps(payload))\n"
174
+ )
175
+
176
+
177
+ def _add_basic_tests(project_path: Path) -> None:
178
+ """Add a basic test file."""
179
+ tests_dir = project_path / "tests"
180
+ tests_dir.mkdir(exist_ok=True)
181
+
182
+ test_file = tests_dir / "test_handler.py"
183
+ if not test_file.exists():
184
+ test_file.write_text(
185
+ '"""Basic tests for the Lambda handler."""\n'
186
+ "import json\n"
187
+ "from src.handler import main\n\n\n"
188
+ "def test_handler_returns_200():\n"
189
+ ' event = {"httpMethod": "GET", "path": "/"}\n'
190
+ " context = {}\n"
191
+ " response = main(event, context)\n"
192
+ ' assert response["statusCode"] == 200\n'
193
+ )
194
+
195
+ # Add pytest config
196
+ pytest_ini = project_path / "pytest.ini"
197
+ if not pytest_ini.exists():
198
+ pytest_ini.write_text(
199
+ "[pytest]\n"
200
+ "testpaths = tests\n"
201
+ "python_files = test_*.py\n"
202
+ "python_classes = Test*\n"
203
+ "python_functions = test_*\n"
204
+ )
205
+
206
+
207
+ def _add_github_actions(project_path: Path) -> None:
208
+ """Add a basic GitHub Actions workflow."""
209
+ workflows_dir = project_path / ".github" / "workflows"
210
+ workflows_dir.mkdir(parents=True, exist_ok=True)
211
+
212
+ workflow_file = workflows_dir / "ci.yml"
213
+ if not workflow_file.exists():
214
+ workflow_file.write_text(
215
+ "name: CI\n\n"
216
+ "on:\n"
217
+ " push:\n"
218
+ " branches: [main]\n"
219
+ " pull_request:\n"
220
+ " branches: [main]\n\n"
221
+ "jobs:\n"
222
+ " test:\n"
223
+ " runs-on: ubuntu-latest\n"
224
+ " steps:\n"
225
+ " - uses: actions/checkout@v4\n"
226
+ " - uses: actions/setup-python@v5\n"
227
+ " with:\n"
228
+ " python-version: '3.12'\n"
229
+ " - run: pip install -r requirements.txt\n"
230
+ " - run: pytest\n"
231
+ )
@@ -0,0 +1,12 @@
1
+ {
2
+ "project_name": "my-lambda",
3
+ "project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
4
+ "module_name": "{{ cookiecutter.project_slug }}",
5
+ "description": "Serverless API built with AWS Lambda",
6
+ "author_name": "LambdaForge User",
7
+ "author_email": "user@example.com",
8
+ "runtime": "python3.12",
9
+ "aws_region": "us-east-1",
10
+ "memory_mb": 512,
11
+ "timeout_seconds": 30
12
+ }
@@ -0,0 +1,22 @@
1
+ {% raw %}
2
+ name: Dependabot
3
+
4
+ on:
5
+ pull_request:
6
+ push:
7
+
8
+ jobs:
9
+ dependabot:
10
+ runs-on: ubuntu-latest
11
+ if: github.actor == 'dependabot[bot]'
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Approve Dependabot PR
16
+ run: |
17
+ gh pr review --approve "$PR_URL"
18
+ gh pr merge --auto --squash "$PR_URL"
19
+ env:
20
+ PR_URL: ${{ github.event.pull_request.html_url }}
21
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22
+ {% endraw %}