@actuallyjamez/elysian 0.3.0 → 0.5.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/README.md +143 -50
- package/dist/cli/commands/build.js +6 -6
- package/dist/cli/commands/dev.js +2 -2
- package/dist/cli/commands/generate-iac.js +3 -3
- 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 +95 -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 +99 -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 +79 -268
- package/dist/core/config.d.ts +6 -6
- package/dist/core/config.js +22 -8
- package/dist/core/manifest.d.ts +1 -1
- package/dist/core/manifest.js +5 -5
- package/dist/core/naming.d.ts +7 -6
- package/dist/core/naming.js +9 -8
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File scaffolding logic
|
|
3
|
+
*/
|
|
4
|
+
import type { PackageManager, ProjectInfo } from "./detect";
|
|
5
|
+
import type { WizardAnswers } from "./prompts";
|
|
6
|
+
export interface ScaffoldResult {
|
|
7
|
+
created: string[];
|
|
8
|
+
updated: string[];
|
|
9
|
+
skipped: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scaffold all project files
|
|
13
|
+
*/
|
|
14
|
+
export declare function scaffoldProject(cwd: string, info: ProjectInfo, answers: WizardAnswers, force: boolean): Promise<ScaffoldResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Install dependencies using the specified package manager
|
|
17
|
+
*/
|
|
18
|
+
export declare function installDependencies(cwd: string, packageManager: PackageManager): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Get relative path for display
|
|
21
|
+
*/
|
|
22
|
+
export declare function getRelativePath(cwd: string, fullPath: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Print scaffold results
|
|
25
|
+
*/
|
|
26
|
+
export declare function printResults(cwd: string, result: ScaffoldResult): void;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File scaffolding logic
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { spawn } from "bun";
|
|
7
|
+
import consola from "consola";
|
|
8
|
+
import { configTemplate, exampleLambdaTemplate, packageJsonTemplate, gitignoreTemplate, tsconfigTemplate, } from "./templates";
|
|
9
|
+
import { templates as tfTemplates, appendProviders, getMissingVariables, appendMain, appendOutputs, } from "./terraform";
|
|
10
|
+
/**
|
|
11
|
+
* Read file content or return empty string if not exists
|
|
12
|
+
*/
|
|
13
|
+
async function readFileOrEmpty(path) {
|
|
14
|
+
if (!existsSync(path)) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
return await Bun.file(path).text();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Write file and track in result
|
|
21
|
+
*/
|
|
22
|
+
async function writeFile(path, content, result, isUpdate) {
|
|
23
|
+
await Bun.write(path, content);
|
|
24
|
+
if (isUpdate) {
|
|
25
|
+
result.updated.push(path);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
result.created.push(path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scaffold all project files
|
|
33
|
+
*/
|
|
34
|
+
export async function scaffoldProject(cwd, info, answers, force) {
|
|
35
|
+
const result = {
|
|
36
|
+
created: [],
|
|
37
|
+
updated: [],
|
|
38
|
+
skipped: [],
|
|
39
|
+
};
|
|
40
|
+
// Create directories
|
|
41
|
+
const lambdasDir = join(cwd, "src/lambdas");
|
|
42
|
+
const terraformDir = join(cwd, "terraform");
|
|
43
|
+
if (!existsSync(lambdasDir)) {
|
|
44
|
+
mkdirSync(lambdasDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
if (!existsSync(terraformDir)) {
|
|
47
|
+
mkdirSync(terraformDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
// For fresh projects, create package.json, .gitignore, tsconfig.json
|
|
50
|
+
if (info.isEmpty) {
|
|
51
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
52
|
+
await writeFile(packageJsonPath, packageJsonTemplate(answers.name), result, false);
|
|
53
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
54
|
+
if (!existsSync(gitignorePath)) {
|
|
55
|
+
await writeFile(gitignorePath, gitignoreTemplate(), result, false);
|
|
56
|
+
}
|
|
57
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
58
|
+
if (!existsSync(tsconfigPath)) {
|
|
59
|
+
await writeFile(tsconfigPath, tsconfigTemplate(), result, false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Create elysian.config.ts
|
|
63
|
+
const configPath = join(cwd, "elysian.config.ts");
|
|
64
|
+
if (!existsSync(configPath) || force) {
|
|
65
|
+
await writeFile(configPath, configTemplate(answers.name), result, existsSync(configPath));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result.skipped.push(configPath);
|
|
69
|
+
}
|
|
70
|
+
// Create example lambda (only if no lambda files exist)
|
|
71
|
+
const exampleLambdaPath = join(lambdasDir, "hello.ts");
|
|
72
|
+
if (!info.hasLambdaFiles) {
|
|
73
|
+
await writeFile(exampleLambdaPath, exampleLambdaTemplate(), result, false);
|
|
74
|
+
}
|
|
75
|
+
// Handle Terraform files
|
|
76
|
+
await scaffoldTerraform(cwd, info, answers.name, result);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Scaffold Terraform files with smart appending
|
|
81
|
+
*/
|
|
82
|
+
async function scaffoldTerraform(cwd, info, name, result) {
|
|
83
|
+
const tfDir = join(cwd, "terraform");
|
|
84
|
+
// providers.tf
|
|
85
|
+
const providersPath = join(tfDir, "providers.tf");
|
|
86
|
+
if (info.terraformFiles.providers) {
|
|
87
|
+
const existing = await readFileOrEmpty(providersPath);
|
|
88
|
+
const updated = appendProviders(existing);
|
|
89
|
+
if (updated !== existing) {
|
|
90
|
+
await writeFile(providersPath, updated, result, true);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result.skipped.push(providersPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
await writeFile(providersPath, tfTemplates.providers, result, false);
|
|
98
|
+
}
|
|
99
|
+
// variables.tf
|
|
100
|
+
const variablesPath = join(tfDir, "variables.tf");
|
|
101
|
+
if (info.terraformFiles.variables) {
|
|
102
|
+
const existing = await readFileOrEmpty(variablesPath);
|
|
103
|
+
const updated = getMissingVariables(existing, name);
|
|
104
|
+
if (updated !== existing) {
|
|
105
|
+
await writeFile(variablesPath, updated, result, true);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
result.skipped.push(variablesPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await writeFile(variablesPath, tfTemplates.variables(name), result, false);
|
|
113
|
+
}
|
|
114
|
+
// main.tf
|
|
115
|
+
const mainPath = join(tfDir, "main.tf");
|
|
116
|
+
if (info.terraformFiles.main) {
|
|
117
|
+
const existing = await readFileOrEmpty(mainPath);
|
|
118
|
+
const updated = appendMain(existing);
|
|
119
|
+
if (updated !== existing) {
|
|
120
|
+
await writeFile(mainPath, updated, result, true);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
result.skipped.push(mainPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await writeFile(mainPath, tfTemplates.main, result, false);
|
|
128
|
+
}
|
|
129
|
+
// outputs.tf
|
|
130
|
+
const outputsPath = join(tfDir, "outputs.tf");
|
|
131
|
+
if (info.terraformFiles.outputs) {
|
|
132
|
+
const existing = await readFileOrEmpty(outputsPath);
|
|
133
|
+
const updated = appendOutputs(existing);
|
|
134
|
+
if (updated !== existing) {
|
|
135
|
+
await writeFile(outputsPath, updated, result, true);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
result.skipped.push(outputsPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
await writeFile(outputsPath, tfTemplates.outputs, result, false);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Install dependencies using the specified package manager
|
|
147
|
+
*/
|
|
148
|
+
export async function installDependencies(cwd, packageManager) {
|
|
149
|
+
const deps = ["elysia", "@actuallyjamez/elysian"];
|
|
150
|
+
const devDeps = ["@types/bun", "typescript"];
|
|
151
|
+
const addCmd = packageManager === "npm" ? "install" : "add";
|
|
152
|
+
const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
|
|
153
|
+
consola.start("Installing dependencies...");
|
|
154
|
+
// Install main dependencies
|
|
155
|
+
const addProc = spawn([packageManager, addCmd, ...deps], {
|
|
156
|
+
cwd,
|
|
157
|
+
stdout: "ignore",
|
|
158
|
+
stderr: "pipe",
|
|
159
|
+
});
|
|
160
|
+
await addProc.exited;
|
|
161
|
+
if (addProc.exitCode !== 0) {
|
|
162
|
+
const stderr = await new Response(addProc.stderr).text();
|
|
163
|
+
throw new Error(`Failed to install dependencies: ${stderr}`);
|
|
164
|
+
}
|
|
165
|
+
// Install dev dependencies
|
|
166
|
+
const devProc = spawn([packageManager, addCmd, devFlag, ...devDeps], {
|
|
167
|
+
cwd,
|
|
168
|
+
stdout: "ignore",
|
|
169
|
+
stderr: "pipe",
|
|
170
|
+
});
|
|
171
|
+
await devProc.exited;
|
|
172
|
+
if (devProc.exitCode !== 0) {
|
|
173
|
+
const stderr = await new Response(devProc.stderr).text();
|
|
174
|
+
throw new Error(`Failed to install dev dependencies: ${stderr}`);
|
|
175
|
+
}
|
|
176
|
+
consola.success("Dependencies installed");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get relative path for display
|
|
180
|
+
*/
|
|
181
|
+
export function getRelativePath(cwd, fullPath) {
|
|
182
|
+
return fullPath.replace(cwd + "/", "");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Print scaffold results
|
|
186
|
+
*/
|
|
187
|
+
export function printResults(cwd, result) {
|
|
188
|
+
console.log("");
|
|
189
|
+
for (const path of result.created) {
|
|
190
|
+
consola.success(`Created ${getRelativePath(cwd, path)}`);
|
|
191
|
+
}
|
|
192
|
+
for (const path of result.updated) {
|
|
193
|
+
consola.success(`Updated ${getRelativePath(cwd, path)}`);
|
|
194
|
+
}
|
|
195
|
+
// Don't print skipped files - too noisy
|
|
196
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template strings for generated files
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* elysian.config.ts template
|
|
6
|
+
*/
|
|
7
|
+
export declare function configTemplate(name: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Example lambda template
|
|
10
|
+
*/
|
|
11
|
+
export declare function exampleLambdaTemplate(): string;
|
|
12
|
+
/**
|
|
13
|
+
* package.json template for fresh projects
|
|
14
|
+
*/
|
|
15
|
+
export declare function packageJsonTemplate(name: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* .gitignore template
|
|
18
|
+
*/
|
|
19
|
+
export declare function gitignoreTemplate(): string;
|
|
20
|
+
/**
|
|
21
|
+
* tsconfig.json template
|
|
22
|
+
*/
|
|
23
|
+
export declare function tsconfigTemplate(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template strings for generated files
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* elysian.config.ts template
|
|
6
|
+
*/
|
|
7
|
+
export function configTemplate(name) {
|
|
8
|
+
return `import { defineConfig } from "@actuallyjamez/elysian";
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
name: "${name}",
|
|
12
|
+
});
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Example lambda template
|
|
17
|
+
*/
|
|
18
|
+
export function exampleLambdaTemplate() {
|
|
19
|
+
return `import { createLambda, t } from "@actuallyjamez/elysian";
|
|
20
|
+
|
|
21
|
+
export default createLambda()
|
|
22
|
+
.get("/hello", ({ query }) => {
|
|
23
|
+
return \`Hello, \${query.name ?? "World"}!\`;
|
|
24
|
+
}, {
|
|
25
|
+
response: t.String(),
|
|
26
|
+
query: t.Object({
|
|
27
|
+
name: t.Optional(t.String()),
|
|
28
|
+
}),
|
|
29
|
+
detail: {
|
|
30
|
+
summary: "Say hello",
|
|
31
|
+
tags: ["Greeting"],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* package.json template for fresh projects
|
|
38
|
+
*/
|
|
39
|
+
export function packageJsonTemplate(name) {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
name: name,
|
|
42
|
+
version: "0.1.0",
|
|
43
|
+
type: "module",
|
|
44
|
+
scripts: {
|
|
45
|
+
build: "elysian build",
|
|
46
|
+
dev: "elysian dev",
|
|
47
|
+
},
|
|
48
|
+
}, null, 2);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* .gitignore template
|
|
52
|
+
*/
|
|
53
|
+
export function gitignoreTemplate() {
|
|
54
|
+
return `# Dependencies
|
|
55
|
+
node_modules/
|
|
56
|
+
|
|
57
|
+
# Build output
|
|
58
|
+
dist/
|
|
59
|
+
|
|
60
|
+
# Environment
|
|
61
|
+
.env
|
|
62
|
+
.env.local
|
|
63
|
+
|
|
64
|
+
# Terraform
|
|
65
|
+
terraform/.terraform/
|
|
66
|
+
terraform/*.tfstate
|
|
67
|
+
terraform/*.tfstate.backup
|
|
68
|
+
terraform/.terraform.lock.hcl
|
|
69
|
+
|
|
70
|
+
# IDE
|
|
71
|
+
.idea/
|
|
72
|
+
.vscode/
|
|
73
|
+
*.swp
|
|
74
|
+
*.swo
|
|
75
|
+
|
|
76
|
+
# OS
|
|
77
|
+
.DS_Store
|
|
78
|
+
Thumbs.db
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* tsconfig.json template
|
|
83
|
+
*/
|
|
84
|
+
export function tsconfigTemplate() {
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
compilerOptions: {
|
|
87
|
+
target: "ESNext",
|
|
88
|
+
module: "ESNext",
|
|
89
|
+
moduleResolution: "bundler",
|
|
90
|
+
strict: true,
|
|
91
|
+
esModuleInterop: true,
|
|
92
|
+
skipLibCheck: true,
|
|
93
|
+
noEmit: true,
|
|
94
|
+
types: ["bun-types"],
|
|
95
|
+
},
|
|
96
|
+
include: ["src/**/*", "elysian.config.ts"],
|
|
97
|
+
exclude: ["node_modules", "dist"],
|
|
98
|
+
}, null, 2);
|
|
99
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Terraform file handling - append missing blocks
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if content has AWS provider configured
|
|
6
|
+
*/
|
|
7
|
+
export declare function hasAwsProvider(content: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Check if a variable exists in the content
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasVariable(content: string, name: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a resource of a given type exists
|
|
14
|
+
*/
|
|
15
|
+
export declare function hasResource(content: string, type: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Check if an output exists in the content
|
|
18
|
+
*/
|
|
19
|
+
export declare function hasOutput(content: string, name: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Smart append to providers.tf - only add if AWS provider is missing
|
|
22
|
+
*/
|
|
23
|
+
export declare function appendProviders(existing: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Get missing variables and return the block to append
|
|
26
|
+
*/
|
|
27
|
+
export declare function getMissingVariables(existing: string, name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Smart append to main.tf - only add if API Gateway resource is missing
|
|
30
|
+
*/
|
|
31
|
+
export declare function appendMain(existing: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Smart append to outputs.tf - only add if api_endpoint output is missing
|
|
34
|
+
*/
|
|
35
|
+
export declare function appendOutputs(existing: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Full templates for new files
|
|
38
|
+
*/
|
|
39
|
+
export declare const templates: {
|
|
40
|
+
providers: string;
|
|
41
|
+
variables: (name: string) => string;
|
|
42
|
+
main: string;
|
|
43
|
+
outputs: string;
|
|
44
|
+
};
|
|
@@ -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, name) {
|
|
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 = "${name}"
|
|
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: (name) => `# 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 = "${name}"
|
|
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;
|