@actuallyjamez/elysian 0.2.1 → 0.4.0
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.
- package/dist/cli/commands/init/detect.d.ts +33 -0
- package/dist/cli/commands/init/detect.js +98 -0
- package/dist/cli/commands/init/index.d.ts +6 -0
- package/dist/cli/commands/init/index.js +6 -0
- package/dist/cli/commands/init/prompts.d.ts +23 -0
- package/dist/cli/commands/init/prompts.js +93 -0
- package/dist/cli/commands/init/scaffold.d.ts +26 -0
- package/dist/cli/commands/init/scaffold.js +196 -0
- package/dist/cli/commands/init/templates.d.ts +23 -0
- package/dist/cli/commands/init/templates.js +103 -0
- package/dist/cli/commands/init/terraform.d.ts +44 -0
- package/dist/cli/commands/init/terraform.js +309 -0
- package/dist/cli/commands/init.d.ts +1 -6
- package/dist/cli/commands/init.js +70 -266
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Terraform file handling - append missing blocks
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if content has AWS provider configured
|
|
6
|
+
*/
|
|
7
|
+
export function hasAwsProvider(content) {
|
|
8
|
+
return /source\s*=\s*["']hashicorp\/aws["']/.test(content);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if a variable exists in the content
|
|
12
|
+
*/
|
|
13
|
+
export function hasVariable(content, name) {
|
|
14
|
+
const regex = new RegExp(`variable\\s+["']${name}["']`, "i");
|
|
15
|
+
return regex.test(content);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if a resource of a given type exists
|
|
19
|
+
*/
|
|
20
|
+
export function hasResource(content, type) {
|
|
21
|
+
const regex = new RegExp(`resource\\s+["']${type}["']`, "i");
|
|
22
|
+
return regex.test(content);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if an output exists in the content
|
|
26
|
+
*/
|
|
27
|
+
export function hasOutput(content, name) {
|
|
28
|
+
const regex = new RegExp(`output\\s+["']${name}["']`, "i");
|
|
29
|
+
return regex.test(content);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Providers template
|
|
33
|
+
*/
|
|
34
|
+
const PROVIDERS_BLOCK = `
|
|
35
|
+
# Elysian: AWS Provider Configuration
|
|
36
|
+
terraform {
|
|
37
|
+
required_providers {
|
|
38
|
+
aws = {
|
|
39
|
+
source = "hashicorp/aws"
|
|
40
|
+
version = "~> 6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
provider "aws" {
|
|
46
|
+
region = var.region
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
/**
|
|
50
|
+
* Smart append to providers.tf - only add if AWS provider is missing
|
|
51
|
+
*/
|
|
52
|
+
export function appendProviders(existing) {
|
|
53
|
+
if (hasAwsProvider(existing)) {
|
|
54
|
+
return existing; // Already has AWS provider
|
|
55
|
+
}
|
|
56
|
+
return existing + "\n" + PROVIDERS_BLOCK;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get missing variables and return the block to append
|
|
60
|
+
*/
|
|
61
|
+
export function getMissingVariables(existing, apiName) {
|
|
62
|
+
const variables = {
|
|
63
|
+
region: `
|
|
64
|
+
variable "region" {
|
|
65
|
+
type = string
|
|
66
|
+
default = "eu-west-2"
|
|
67
|
+
}`,
|
|
68
|
+
lambda_names: `
|
|
69
|
+
variable "lambda_names" {
|
|
70
|
+
type = list(string)
|
|
71
|
+
default = []
|
|
72
|
+
}`,
|
|
73
|
+
api_routes: `
|
|
74
|
+
variable "api_routes" {
|
|
75
|
+
type = map(object({
|
|
76
|
+
lambda_key = string
|
|
77
|
+
route_key = string
|
|
78
|
+
path_parameters = list(string)
|
|
79
|
+
}))
|
|
80
|
+
default = {}
|
|
81
|
+
}`,
|
|
82
|
+
lambda_runtime: `
|
|
83
|
+
variable "lambda_runtime" {
|
|
84
|
+
type = string
|
|
85
|
+
default = "nodejs22.x"
|
|
86
|
+
}`,
|
|
87
|
+
lambda_memory_size: `
|
|
88
|
+
variable "lambda_memory_size" {
|
|
89
|
+
type = number
|
|
90
|
+
default = 256
|
|
91
|
+
}`,
|
|
92
|
+
lambda_timeout: `
|
|
93
|
+
variable "lambda_timeout" {
|
|
94
|
+
type = number
|
|
95
|
+
default = 30
|
|
96
|
+
}`,
|
|
97
|
+
api_name: `
|
|
98
|
+
variable "api_name" {
|
|
99
|
+
type = string
|
|
100
|
+
default = "${apiName}"
|
|
101
|
+
}`,
|
|
102
|
+
tags: `
|
|
103
|
+
variable "tags" {
|
|
104
|
+
type = map(string)
|
|
105
|
+
default = {}
|
|
106
|
+
}`,
|
|
107
|
+
};
|
|
108
|
+
const missing = [];
|
|
109
|
+
for (const [name, block] of Object.entries(variables)) {
|
|
110
|
+
if (!hasVariable(existing, name)) {
|
|
111
|
+
missing.push(block);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (missing.length === 0) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
return existing + "\n# Elysian: Required Variables" + missing.join("");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Main resources template
|
|
121
|
+
*/
|
|
122
|
+
const MAIN_RESOURCES = `
|
|
123
|
+
# Elysian: Lambda and API Gateway Resources
|
|
124
|
+
|
|
125
|
+
locals {
|
|
126
|
+
lambda_functions = {
|
|
127
|
+
for name in var.lambda_names : name => {
|
|
128
|
+
filename = "\${path.module}/../dist/\${name}.zip"
|
|
129
|
+
handler = "index.handler"
|
|
130
|
+
source_code_hash = filebase64sha256("\${path.module}/../dist/\${name}.zip")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# API Gateway
|
|
136
|
+
resource "aws_apigatewayv2_api" "elysian" {
|
|
137
|
+
name = var.api_name
|
|
138
|
+
protocol_type = "HTTP"
|
|
139
|
+
tags = var.tags
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# IAM Role for Lambda
|
|
143
|
+
resource "aws_iam_role" "elysian_lambda" {
|
|
144
|
+
name = "\${var.api_name}-lambda-role"
|
|
145
|
+
|
|
146
|
+
assume_role_policy = jsonencode({
|
|
147
|
+
Version = "2012-10-17"
|
|
148
|
+
Statement = [{
|
|
149
|
+
Action = "sts:AssumeRole"
|
|
150
|
+
Effect = "Allow"
|
|
151
|
+
Principal = {
|
|
152
|
+
Service = "lambda.amazonaws.com"
|
|
153
|
+
}
|
|
154
|
+
}]
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
tags = var.tags
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resource "aws_iam_role_policy_attachment" "elysian_lambda_basic" {
|
|
161
|
+
role = aws_iam_role.elysian_lambda.name
|
|
162
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Lambda Functions
|
|
166
|
+
resource "aws_lambda_function" "elysian" {
|
|
167
|
+
for_each = local.lambda_functions
|
|
168
|
+
|
|
169
|
+
filename = each.value.filename
|
|
170
|
+
function_name = each.key
|
|
171
|
+
role = aws_iam_role.elysian_lambda.arn
|
|
172
|
+
handler = each.value.handler
|
|
173
|
+
source_code_hash = each.value.source_code_hash
|
|
174
|
+
runtime = var.lambda_runtime
|
|
175
|
+
memory_size = var.lambda_memory_size
|
|
176
|
+
timeout = var.lambda_timeout
|
|
177
|
+
|
|
178
|
+
tags = var.tags
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# API Gateway Integrations
|
|
182
|
+
resource "aws_apigatewayv2_integration" "elysian" {
|
|
183
|
+
for_each = local.lambda_functions
|
|
184
|
+
|
|
185
|
+
api_id = aws_apigatewayv2_api.elysian.id
|
|
186
|
+
integration_type = "AWS_PROXY"
|
|
187
|
+
integration_uri = aws_lambda_function.elysian[each.key].invoke_arn
|
|
188
|
+
integration_method = "POST"
|
|
189
|
+
payload_format_version = "2.0"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Lambda Permissions
|
|
193
|
+
resource "aws_lambda_permission" "elysian_apigateway" {
|
|
194
|
+
for_each = local.lambda_functions
|
|
195
|
+
|
|
196
|
+
statement_id = "AllowAPIGateway"
|
|
197
|
+
action = "lambda:InvokeFunction"
|
|
198
|
+
function_name = aws_lambda_function.elysian[each.key].function_name
|
|
199
|
+
principal = "apigateway.amazonaws.com"
|
|
200
|
+
source_arn = "\${aws_apigatewayv2_api.elysian.execution_arn}/*/*"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# API Gateway Routes
|
|
204
|
+
resource "aws_apigatewayv2_route" "elysian" {
|
|
205
|
+
for_each = var.api_routes
|
|
206
|
+
|
|
207
|
+
api_id = aws_apigatewayv2_api.elysian.id
|
|
208
|
+
route_key = each.value.route_key
|
|
209
|
+
target = "integrations/\${aws_apigatewayv2_integration.elysian[each.value.lambda_key].id}"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# API Gateway Stage
|
|
213
|
+
resource "aws_apigatewayv2_stage" "elysian" {
|
|
214
|
+
api_id = aws_apigatewayv2_api.elysian.id
|
|
215
|
+
name = "$default"
|
|
216
|
+
auto_deploy = true
|
|
217
|
+
tags = var.tags
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
/**
|
|
221
|
+
* Smart append to main.tf - only add if API Gateway resource is missing
|
|
222
|
+
*/
|
|
223
|
+
export function appendMain(existing) {
|
|
224
|
+
// Check for our specific resource or any API Gateway
|
|
225
|
+
if (hasResource(existing, "aws_apigatewayv2_api") ||
|
|
226
|
+
hasResource(existing, "aws_lambda_function")) {
|
|
227
|
+
return existing; // Already has Lambda/API Gateway resources
|
|
228
|
+
}
|
|
229
|
+
return existing + MAIN_RESOURCES;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Outputs template
|
|
233
|
+
*/
|
|
234
|
+
const OUTPUTS_BLOCK = `
|
|
235
|
+
# Elysian: API Endpoint Output
|
|
236
|
+
output "api_endpoint" {
|
|
237
|
+
description = "API Gateway endpoint URL"
|
|
238
|
+
value = aws_apigatewayv2_stage.elysian.invoke_url
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
/**
|
|
242
|
+
* Smart append to outputs.tf - only add if api_endpoint output is missing
|
|
243
|
+
*/
|
|
244
|
+
export function appendOutputs(existing) {
|
|
245
|
+
if (hasOutput(existing, "api_endpoint")) {
|
|
246
|
+
return existing;
|
|
247
|
+
}
|
|
248
|
+
return existing + OUTPUTS_BLOCK;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Full templates for new files
|
|
252
|
+
*/
|
|
253
|
+
export const templates = {
|
|
254
|
+
providers: PROVIDERS_BLOCK.trim() + "\n",
|
|
255
|
+
variables: (apiName) => `# Elysian: Terraform Variables
|
|
256
|
+
|
|
257
|
+
variable "region" {
|
|
258
|
+
type = string
|
|
259
|
+
default = "eu-west-2"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
variable "lambda_names" {
|
|
263
|
+
type = list(string)
|
|
264
|
+
default = []
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
variable "api_routes" {
|
|
268
|
+
type = map(object({
|
|
269
|
+
lambda_key = string
|
|
270
|
+
route_key = string
|
|
271
|
+
path_parameters = list(string)
|
|
272
|
+
}))
|
|
273
|
+
default = {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
variable "lambda_runtime" {
|
|
277
|
+
type = string
|
|
278
|
+
default = "nodejs22.x"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
variable "lambda_memory_size" {
|
|
282
|
+
type = number
|
|
283
|
+
default = 256
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
variable "lambda_timeout" {
|
|
287
|
+
type = number
|
|
288
|
+
default = 30
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
variable "api_name" {
|
|
292
|
+
type = string
|
|
293
|
+
default = "${apiName}"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
variable "tags" {
|
|
297
|
+
type = map(string)
|
|
298
|
+
default = {}
|
|
299
|
+
}
|
|
300
|
+
`,
|
|
301
|
+
main: MAIN_RESOURCES.trim() + "\n",
|
|
302
|
+
outputs: `# Elysian: Outputs
|
|
303
|
+
|
|
304
|
+
output "api_endpoint" {
|
|
305
|
+
description = "API Gateway endpoint URL"
|
|
306
|
+
value = aws_apigatewayv2_stage.elysian.invoke_url
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
};
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Init command -
|
|
2
|
+
* Init command - Interactive wizard to initialize elysian projects
|
|
3
3
|
*/
|
|
4
4
|
export declare const initCommand: import("citty").CommandDef<{
|
|
5
|
-
name: {
|
|
6
|
-
type: "string";
|
|
7
|
-
description: string;
|
|
8
|
-
default: string;
|
|
9
|
-
};
|
|
10
5
|
force: {
|
|
11
6
|
type: "boolean";
|
|
12
7
|
description: string;
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Init command -
|
|
2
|
+
* Init command - Interactive wizard to initialize elysian projects
|
|
3
3
|
*/
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import consola from "consola";
|
|
6
6
|
import { existsSync, mkdirSync } from "fs";
|
|
7
|
-
import {
|
|
7
|
+
import { resolve, basename } from "path";
|
|
8
|
+
import { detectProject, } from "./init/detect";
|
|
9
|
+
import { promptTargetDirectory, runFreshProjectWizard, runExistingProjectWizard, } from "./init/prompts";
|
|
10
|
+
import { scaffoldProject, installDependencies, printResults, } from "./init/scaffold";
|
|
8
11
|
export const initCommand = defineCommand({
|
|
9
12
|
meta: {
|
|
10
13
|
name: "init",
|
|
11
14
|
description: "Initialize a new elysian project",
|
|
12
15
|
},
|
|
13
16
|
args: {
|
|
14
|
-
name: {
|
|
15
|
-
type: "string",
|
|
16
|
-
description: "API name",
|
|
17
|
-
default: "my-api",
|
|
18
|
-
},
|
|
19
17
|
force: {
|
|
20
18
|
type: "boolean",
|
|
21
19
|
description: "Overwrite existing files",
|
|
@@ -23,273 +21,79 @@ export const initCommand = defineCommand({
|
|
|
23
21
|
},
|
|
24
22
|
},
|
|
25
23
|
async run({ args }) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
const originalCwd = process.cwd();
|
|
25
|
+
// Step 1: Get target directory
|
|
26
|
+
const targetDir = await promptTargetDirectory(basename(originalCwd));
|
|
27
|
+
if (!targetDir) {
|
|
28
|
+
consola.info("Cancelled");
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
// Resolve the target directory
|
|
32
|
+
const cwd = resolve(originalCwd, targetDir);
|
|
33
|
+
const apiName = basename(cwd);
|
|
34
|
+
// Create directory if it doesn't exist
|
|
35
|
+
if (!existsSync(cwd)) {
|
|
36
|
+
mkdirSync(cwd, { recursive: true });
|
|
37
|
+
consola.success(`Created directory: ${targetDir}`);
|
|
38
|
+
}
|
|
39
|
+
// Detect project state in target directory
|
|
40
|
+
const info = await detectProject(cwd);
|
|
41
|
+
// Check for existing config (unless force)
|
|
42
|
+
if (info.hasElysianConfig && !args.force) {
|
|
32
43
|
consola.error("elysian.config.ts already exists. Use --force to overwrite.");
|
|
33
44
|
process.exit(1);
|
|
34
45
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
// Terraform configuration
|
|
63
|
-
terraform: {
|
|
64
|
-
outputDir: "terraform",
|
|
65
|
-
tfvarsFilename: "api-routes.auto.tfvars",
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
// Lambda defaults
|
|
69
|
-
lambda: {
|
|
70
|
-
runtime: "nodejs20.x",
|
|
71
|
-
memorySize: 256,
|
|
72
|
-
timeout: 30,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
`;
|
|
76
|
-
await Bun.write(configPath, configContent);
|
|
77
|
-
consola.success("Created elysian.config.ts");
|
|
78
|
-
// Write example lambda
|
|
79
|
-
const exampleLambdaPath = join(lambdasDir, "hello.ts");
|
|
80
|
-
if (!existsSync(exampleLambdaPath) || args.force) {
|
|
81
|
-
const exampleLambdaContent = `import { createLambda, t } from "@actuallyjamez/elysian";
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Example Lambda - Hello World
|
|
85
|
-
*
|
|
86
|
-
* Routes defined here will be automatically:
|
|
87
|
-
* - Bundled into a Lambda function
|
|
88
|
-
* - Mapped to API Gateway routes
|
|
89
|
-
* - Included in OpenAPI documentation
|
|
90
|
-
*/
|
|
91
|
-
export default createLambda()
|
|
92
|
-
.get("/hello", ({ query }) => {
|
|
93
|
-
return \`Hello, \${query.name ?? "World"}!\`;
|
|
94
|
-
}, {
|
|
95
|
-
response: t.String(),
|
|
96
|
-
query: t.Object({
|
|
97
|
-
name: t.Optional(t.String()),
|
|
98
|
-
}),
|
|
99
|
-
detail: {
|
|
100
|
-
summary: "Say hello",
|
|
101
|
-
description: "Returns a greeting message",
|
|
102
|
-
tags: ["Greeting"],
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
`;
|
|
106
|
-
await Bun.write(exampleLambdaPath, exampleLambdaContent);
|
|
107
|
-
consola.success("Created src/lambdas/hello.ts");
|
|
46
|
+
// Run appropriate wizard based on whether directory is empty
|
|
47
|
+
let answers;
|
|
48
|
+
if (info.isEmpty) {
|
|
49
|
+
const result = await runFreshProjectWizard(apiName);
|
|
50
|
+
if (!result) {
|
|
51
|
+
consola.info("Cancelled");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
answers = {
|
|
55
|
+
targetDir,
|
|
56
|
+
apiName,
|
|
57
|
+
...result,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const result = await runExistingProjectWizard(apiName, info.packageManager);
|
|
62
|
+
if (!result) {
|
|
63
|
+
consola.info("Cancelled");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
answers = {
|
|
67
|
+
targetDir,
|
|
68
|
+
apiName,
|
|
69
|
+
...result,
|
|
70
|
+
};
|
|
108
71
|
}
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
region = var.region
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
variable "region" {
|
|
126
|
-
type = string
|
|
127
|
-
default = "eu-west-2"
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
variable "lambda_names" {
|
|
131
|
-
type = list(string)
|
|
132
|
-
default = []
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
variable "api_routes" {
|
|
136
|
-
type = map(object({
|
|
137
|
-
lambda_key = string
|
|
138
|
-
route_key = string
|
|
139
|
-
path_parameters = list(string)
|
|
140
|
-
}))
|
|
141
|
-
default = {}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
variable "lambda_runtime" {
|
|
145
|
-
type = string
|
|
146
|
-
default = "nodejs20.x"
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
variable "lambda_memory_size" {
|
|
150
|
-
type = number
|
|
151
|
-
default = 256
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
variable "lambda_timeout" {
|
|
155
|
-
type = number
|
|
156
|
-
default = 30
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
variable "api_name" {
|
|
160
|
-
type = string
|
|
161
|
-
default = "${apiName}"
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
variable "tags" {
|
|
165
|
-
type = map(string)
|
|
166
|
-
default = {}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
locals {
|
|
170
|
-
lambda_functions = {
|
|
171
|
-
for name in var.lambda_names : name => {
|
|
172
|
-
filename = "\${path.module}/../dist/\${name}.zip"
|
|
173
|
-
handler = "index.handler"
|
|
174
|
-
source_code_hash = filebase64sha256("\${path.module}/../dist/\${name}.zip")
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
# API Gateway
|
|
180
|
-
resource "aws_apigatewayv2_api" "this" {
|
|
181
|
-
name = var.api_name
|
|
182
|
-
protocol_type = "HTTP"
|
|
183
|
-
description = "API Gateway for \${var.api_name}"
|
|
184
|
-
tags = var.tags
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
# IAM Role for Lambda
|
|
188
|
-
resource "aws_iam_role" "lambda" {
|
|
189
|
-
name = "\${var.api_name}-lambda-role"
|
|
190
|
-
|
|
191
|
-
assume_role_policy = jsonencode({
|
|
192
|
-
Version = "2012-10-17"
|
|
193
|
-
Statement = [{
|
|
194
|
-
Action = "sts:AssumeRole"
|
|
195
|
-
Effect = "Allow"
|
|
196
|
-
Principal = {
|
|
197
|
-
Service = "lambda.amazonaws.com"
|
|
198
|
-
}
|
|
199
|
-
}]
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
tags = var.tags
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
resource "aws_iam_role_policy_attachment" "lambda_basic" {
|
|
206
|
-
role = aws_iam_role.lambda.name
|
|
207
|
-
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
# Lambda Functions
|
|
211
|
-
resource "aws_lambda_function" "this" {
|
|
212
|
-
for_each = local.lambda_functions
|
|
213
|
-
|
|
214
|
-
filename = each.value.filename
|
|
215
|
-
function_name = "\${var.api_name}-\${each.key}"
|
|
216
|
-
role = aws_iam_role.lambda.arn
|
|
217
|
-
handler = each.value.handler
|
|
218
|
-
source_code_hash = each.value.source_code_hash
|
|
219
|
-
runtime = var.lambda_runtime
|
|
220
|
-
memory_size = var.lambda_memory_size
|
|
221
|
-
timeout = var.lambda_timeout
|
|
222
|
-
|
|
223
|
-
tags = var.tags
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
# API Gateway Integrations
|
|
227
|
-
resource "aws_apigatewayv2_integration" "this" {
|
|
228
|
-
for_each = local.lambda_functions
|
|
229
|
-
|
|
230
|
-
api_id = aws_apigatewayv2_api.this.id
|
|
231
|
-
integration_type = "AWS_PROXY"
|
|
232
|
-
integration_uri = aws_lambda_function.this[each.key].invoke_arn
|
|
233
|
-
integration_method = "POST"
|
|
234
|
-
payload_format_version = "2.0"
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# Lambda Permissions
|
|
238
|
-
resource "aws_lambda_permission" "apigateway" {
|
|
239
|
-
for_each = local.lambda_functions
|
|
240
|
-
|
|
241
|
-
statement_id = "AllowExecutionFromAPIGateway-\${each.key}"
|
|
242
|
-
action = "lambda:InvokeFunction"
|
|
243
|
-
function_name = aws_lambda_function.this[each.key].function_name
|
|
244
|
-
principal = "apigateway.amazonaws.com"
|
|
245
|
-
source_arn = "\${aws_apigatewayv2_api.this.execution_arn}/*/*"
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
# API Gateway Routes
|
|
249
|
-
resource "aws_apigatewayv2_route" "this" {
|
|
250
|
-
for_each = var.api_routes
|
|
251
|
-
|
|
252
|
-
api_id = aws_apigatewayv2_api.this.id
|
|
253
|
-
route_key = each.value.route_key
|
|
254
|
-
target = "integrations/\${aws_apigatewayv2_integration.this[each.value.lambda_key].id}"
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
# API Gateway Stage
|
|
258
|
-
resource "aws_apigatewayv2_stage" "this" {
|
|
259
|
-
api_id = aws_apigatewayv2_api.this.id
|
|
260
|
-
name = "$default"
|
|
261
|
-
auto_deploy = true
|
|
262
|
-
|
|
263
|
-
tags = var.tags
|
|
264
|
-
|
|
265
|
-
depends_on = [
|
|
266
|
-
aws_apigatewayv2_route.this,
|
|
267
|
-
aws_lambda_permission.apigateway
|
|
268
|
-
]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
# Outputs
|
|
272
|
-
output "api_endpoint" {
|
|
273
|
-
value = aws_apigatewayv2_stage.this.invoke_url
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
output "lambda_functions" {
|
|
277
|
-
value = { for k, v in aws_lambda_function.this : k => v.arn }
|
|
278
|
-
}
|
|
279
|
-
`;
|
|
280
|
-
await Bun.write(terraformMainPath, terraformContent);
|
|
281
|
-
consola.success("Created terraform/main.tf");
|
|
72
|
+
// Scaffold files
|
|
73
|
+
const scaffoldResult = await scaffoldProject(cwd, info, answers, args.force);
|
|
74
|
+
// Print results
|
|
75
|
+
printResults(cwd, scaffoldResult);
|
|
76
|
+
// Install dependencies if requested
|
|
77
|
+
if (answers.installDeps) {
|
|
78
|
+
try {
|
|
79
|
+
await installDependencies(cwd, answers.packageManager);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
consola.error(error instanceof Error ? error.message : "Failed to install dependencies");
|
|
83
|
+
consola.info(`You can manually install with: ${answers.packageManager} add elysia @actuallyjamez/elysian`);
|
|
84
|
+
}
|
|
282
85
|
}
|
|
283
86
|
// Print next steps
|
|
284
87
|
console.log("");
|
|
88
|
+
const pm = answers.packageManager;
|
|
89
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
90
|
+
// If we created in a subdirectory, tell user to cd into it
|
|
91
|
+
const cdStep = targetDir !== "." ? `cd ${targetDir}\n\n` : "";
|
|
285
92
|
consola.box("Project initialized!\n\n" +
|
|
286
93
|
"Next steps:\n\n" +
|
|
287
|
-
|
|
288
|
-
"
|
|
289
|
-
|
|
290
|
-
"
|
|
291
|
-
" bunx elysian build\n\n" +
|
|
292
|
-
"4. Deploy with Terraform:\n" +
|
|
293
|
-
" cd terraform && terraform init && terraform apply");
|
|
94
|
+
cdStep +
|
|
95
|
+
(answers.installDeps ? "" : `${pm} add elysia @actuallyjamez/elysian\n\n`) +
|
|
96
|
+
`${runCmd} elysian build\n\n` +
|
|
97
|
+
"cd terraform && terraform init && terraform apply");
|
|
294
98
|
},
|
|
295
99
|
});
|
package/dist/core/config.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface BuildConfig {
|
|
|
20
20
|
external?: string[];
|
|
21
21
|
}
|
|
22
22
|
export interface LambdaConfig {
|
|
23
|
-
/** Lambda runtime (default: "
|
|
23
|
+
/** Lambda runtime (default: "nodejs22.x") */
|
|
24
24
|
runtime?: string;
|
|
25
25
|
/** Lambda memory size in MB (default: 256) */
|
|
26
26
|
memorySize?: number;
|