@actuallyjamez/elysian 0.4.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 CHANGED
@@ -1,23 +1,31 @@
1
1
  # elysian
2
2
 
3
- Automatic Lambda bundler for [Elysia](https://elysiajs.com/) with AWS API Gateway and Terraform integration.
3
+ > Automatic Lambda bundler for [Elysia](https://elysiajs.com/) with AWS API Gateway and Terraform integration.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Zero-config Lambda handlers** - Just export your Elysia routes as default, handlers are auto-generated
8
+ - **Interactive init wizard** - Sets up fresh or existing projects with smart defaults
9
+ - **Package manager detection** - Automatically detects bun/npm/pnpm/yarn from lockfiles
8
10
  - **Automatic OpenAPI aggregation** - All routes are aggregated into a single OpenAPI spec endpoint
9
- - **Terraform integration** - Generates `tfvars` files for seamless infrastructure deployment
11
+ - **Smart Terraform integration** - Generates modular Terraform files that won't overwrite your existing config
10
12
  - **Type-safe configuration** - Full TypeScript support with `defineConfig()`
11
13
  - **Watch mode** - Fast rebuilds during development
12
14
 
13
15
  ## Installation
14
16
 
15
17
  ```bash
16
- # Configure GitHub registry for @actuallyjamez scope
17
- echo "@actuallyjamez:registry=https://npm.pkg.github.com" >> .npmrc
18
-
19
- # Install
18
+ # With bun (recommended)
20
19
  bun add elysia @actuallyjamez/elysian
20
+
21
+ # With npm
22
+ npm install elysia @actuallyjamez/elysian
23
+
24
+ # With pnpm
25
+ pnpm add elysia @actuallyjamez/elysian
26
+
27
+ # With yarn
28
+ yarn add elysia @actuallyjamez/elysian
21
29
  ```
22
30
 
23
31
  ## Quick Start
@@ -25,13 +33,41 @@ bun add elysia @actuallyjamez/elysian
25
33
  ### 1. Initialize your project
26
34
 
27
35
  ```bash
28
- bunx @actuallyjamez/elysian init --name my-api
36
+ # Interactive wizard - prompts for everything you need
37
+ elysian init
29
38
  ```
30
39
 
31
- This creates:
32
- - `elysian.config.ts` - Configuration file
33
- - `src/lambdas/hello.ts` - Example lambda
34
- - `terraform/main.tf` - Terraform infrastructure
40
+ The wizard will:
41
+ - Ask where to create the project (default: current directory)
42
+ - Detect your package manager automatically
43
+ - Create all necessary files
44
+ - Install dependencies (fresh projects only)
45
+
46
+ **Fresh project example:**
47
+ ```
48
+ ? Where would you like to create your project? my-api
49
+ ✔ Created directory: my-api
50
+
51
+ ℹ Creating new elysian project: my-api
52
+
53
+ ? Package manager: bun
54
+ ✔ Created package.json
55
+ ✔ Created elysian.config.ts
56
+ ✔ Created src/lambdas/hello.ts
57
+ ✔ Created terraform/providers.tf
58
+ ...
59
+ ```
60
+
61
+ **Existing project example:**
62
+ ```
63
+ ℹ Adding elysian to: my-existing-app
64
+
65
+ ℹ Detected package manager: bun
66
+ ? Install dependencies? Yes
67
+ ✔ Created elysian.config.ts
68
+ ✔ Updated terraform/variables.tf
69
+ ...
70
+ ```
35
71
 
36
72
  ### 2. Write your lambdas
37
73
 
@@ -54,16 +90,16 @@ export default createLambda()
54
90
  });
55
91
  ```
56
92
 
57
- **That's it!** No need to export a handler - the bundler wraps your default export automatically.
93
+ **That's it!** No need to export a handler - bundler wraps your default export automatically.
58
94
 
59
95
  ### 3. Build
60
96
 
61
97
  ```bash
62
98
  # Development build
63
- bunx elysian build
99
+ elysian build
64
100
 
65
101
  # Production build (minified)
66
- bunx elysian build --prod
102
+ elysian build --prod
67
103
  ```
68
104
 
69
105
  ### 4. Deploy
@@ -82,30 +118,32 @@ Create `elysian.config.ts` in your project root:
82
118
  import { defineConfig } from "@actuallyjamez/elysian";
83
119
 
84
120
  export default defineConfig({
85
- // Required
86
- apiName: "my-api",
121
+ // Required: Used for naming your AWS resources
122
+ name: "my-api",
87
123
 
88
- // Optional (showing defaults)
124
+ // Optional: Lambda source directory (default: "src/lambdas")
89
125
  lambdasDir: "src/lambdas",
126
+
127
+ // Optional: Build output directory (default: "dist")
90
128
  outputDir: "dist",
91
129
 
92
- // OpenAPI configuration
130
+ // Optional: OpenAPI configuration (default: enabled)
131
+ // Note: title and version have smart defaults
93
132
  openapi: {
94
133
  enabled: true,
95
- title: "My API",
96
- version: "1.0.0",
134
+ // title defaults to `name` if not provided
135
+ // version defaults to package.json version if not provided
97
136
  description: "API description",
98
137
  },
99
138
 
100
- // Terraform output configuration
139
+ // Optional: Terraform output directory (default: "terraform")
101
140
  terraform: {
102
141
  outputDir: "terraform",
103
- tfvarsFilename: "api-routes.auto.tfvars", // Won't overwrite your existing tfvars
104
142
  },
105
143
 
106
- // Lambda defaults (used in generated tfvars)
144
+ // Optional: Lambda defaults
107
145
  lambda: {
108
- runtime: "nodejs20.x",
146
+ runtime: "nodejs22.x",
109
147
  memorySize: 256,
110
148
  timeout: 30,
111
149
  },
@@ -114,37 +152,66 @@ export default defineConfig({
114
152
 
115
153
  ## CLI Commands
116
154
 
155
+ ### `elysian init`
156
+
157
+ Interactive wizard to initialize a new elysian project.
158
+
159
+ ```bash
160
+ # Initialize in current directory
161
+ elysian init
162
+
163
+ # Initialize in subdirectory (creates it if needed)
164
+ elysian init
165
+ # Answer: my-new-api
166
+
167
+ # Force overwrite existing files
168
+ elysian init --force
169
+ ```
170
+
171
+ **What it creates (fresh project):**
172
+ - `package.json` - With build scripts
173
+ - `tsconfig.json` - TypeScript configuration
174
+ - `.gitignore` - Ignores node_modules, dist, terraform state
175
+ - `elysian.config.ts` - Elysian configuration
176
+ - `src/lambdas/hello.ts` - Example lambda
177
+ - `terraform/providers.tf` - AWS provider configuration
178
+ - `terraform/variables.tf` - Terraform variables
179
+ - `terraform/main.tf` - Lambda and API Gateway resources
180
+ - `terraform/outputs.tf` - API endpoint output
181
+
182
+ **What it does (existing project):**
183
+ - Detects package manager from lockfiles
184
+ - Creates `elysian.config.ts` (doesn't overwrite if exists)
185
+ - Creates example lambda only if no `.ts` files in `src/lambdas/`
186
+ - Smart-appends to existing Terraform files (won't overwrite your config)
187
+
117
188
  ### `elysian build`
118
189
 
119
190
  Build all lambdas for deployment.
120
191
 
121
192
  ```bash
122
- bunx elysian build # Development build
123
- bunx elysian build --prod # Production build (minified)
193
+ # Development build
194
+ elysian build
195
+
196
+ # Production build (minified, no sourcemaps)
197
+ elysian build --prod
124
198
  ```
125
199
 
126
200
  **Output:**
127
201
  - `dist/*.js` - Bundled lambda code
128
202
  - `dist/*.zip` - Lambda deployment packages
129
203
  - `dist/manifest.json` - Route manifest (for debugging)
130
- - `terraform/api-routes.auto.tfvars` - Terraform variables
131
204
 
132
205
  ### `elysian dev`
133
206
 
134
207
  Watch mode for development - rebuilds on file changes.
135
208
 
136
209
  ```bash
137
- bunx elysian dev # Watch with packaging
138
- bunx elysian dev --no-package # Skip zip creation (faster)
139
- ```
210
+ # Watch with packaging
211
+ elysian dev
140
212
 
141
- ### `elysian init`
142
-
143
- Initialize a new project with example files.
144
-
145
- ```bash
146
- bunx elysian init --name my-api
147
- bunx elysian init --force # Overwrite existing files
213
+ # Watch without zip creation (faster)
214
+ elysian dev --no-package
148
215
  ```
149
216
 
150
217
  ### `elysian generate-iac`
@@ -152,15 +219,28 @@ bunx elysian init --force # Overwrite existing files
152
219
  Regenerate Terraform files without rebuilding lambdas.
153
220
 
154
221
  ```bash
155
- bunx elysian generate-iac
222
+ elysian generate-iac
156
223
  ```
157
224
 
225
+ **Smart Terraform file handling:**
226
+ - `providers.tf` - Adds AWS provider if missing
227
+ - `variables.tf` - Adds missing variables only
228
+ - `main.tf` - Adds Lambda/API Gateway resources if missing
229
+ - `outputs.tf` - Adds API endpoint output if missing
230
+
158
231
  ## How It Works
159
232
 
160
233
  ### 1. Route Discovery
161
234
 
162
235
  The bundler scans your `lambdasDir` for `.ts` files. Each file becomes a separate Lambda function.
163
236
 
237
+ ```
238
+ src/lambdas/
239
+ ├── users.ts → users.zip → AWS Lambda (users-api)
240
+ ├── posts.ts → posts.zip → AWS Lambda (posts-api)
241
+ └── auth.ts → auth.zip → AWS Lambda (auth-api)
242
+ ```
243
+
164
244
  ### 2. Handler Injection
165
245
 
166
246
  When you export an Elysia app as default:
@@ -187,7 +267,14 @@ An `__openapi__` lambda is automatically generated that imports all your routes
187
267
 
188
268
  ### 4. Terraform Integration
189
269
 
190
- The generated `tfvars` file contains:
270
+ The generated Terraform files are modular and won't overwrite your existing configuration:
271
+
272
+ - **providers.tf** - AWS provider with version `~> 6.0`
273
+ - **variables.tf** - All variables (region, lambda config, routes)
274
+ - **main.tf** - API Gateway, Lambda, IAM resources
275
+ - **outputs.tf** - API endpoint URL
276
+
277
+ The `terraform/api-routes.auto.tfvars` file is auto-generated on each build with:
191
278
  - List of Lambda names
192
279
  - Route-to-Lambda mappings (with API Gateway path format)
193
280
  - Lambda configuration defaults
@@ -197,22 +284,28 @@ The generated `tfvars` file contains:
197
284
  ```
198
285
  my-api/
199
286
  ├── elysian.config.ts # Configuration
287
+ ├── package.json # Dependencies & scripts
288
+ ├── tsconfig.json # TypeScript config
289
+ ├── .gitignore # Git ignore patterns
200
290
  ├── src/
201
291
  │ └── lambdas/
202
- │ ├── users.ts # → users.zip Lambda
203
- │ ├── posts.ts # → posts.zip Lambda
204
- │ └── auth.ts # → auth.zip Lambda
205
- ├── dist/ # Build output
206
- │ ├── users.js
207
- │ ├── users.zip
208
- │ ├── posts.js
209
- │ ├── posts.zip
210
- │ ├── __openapi__.js # Auto-generated
292
+ │ ├── users.ts # → my-api-users.zip Lambda
293
+ │ ├── posts.ts # → my-api-posts.zip Lambda
294
+ │ └── auth.ts # → my-api-auth.zip Lambda
295
+ ├── dist/ # Build output
296
+ │ ├── my-api-users.js
297
+ │ ├── my-api-users.zip
298
+ │ ├── my-api-posts.js
299
+ │ ├── my-api-posts.zip
300
+ │ ├── __openapi__.js # Auto-generated
211
301
  │ ├── __openapi__.zip
212
302
  │ └── manifest.json
213
303
  └── terraform/
214
- ├── main.tf
215
- └── api-routes.auto.tfvars # Auto-generated
304
+ ├── providers.tf # AWS provider
305
+ ├── variables.tf # Terraform variables
306
+ ├── main.tf # Resources (Lambda, API Gateway, IAM)
307
+ ├── outputs.tf # Outputs
308
+ └── api-routes.auto.tfvars # Auto-generated on build
216
309
  ```
217
310
 
218
311
  ## Requirements
@@ -57,7 +57,7 @@ export const buildCommand = defineCommand({
57
57
  console.log(` ${pc.red("✗")} ${error instanceof Error ? error.message : error}`);
58
58
  process.exit(1);
59
59
  }
60
- const apiName = config.apiName;
60
+ const name = config.name;
61
61
  const lambdasDir = join(process.cwd(), config.lambdasDir);
62
62
  const outputDir = join(process.cwd(), config.outputDir);
63
63
  const terraformDir = join(process.cwd(), config.terraform.outputDir);
@@ -86,7 +86,7 @@ export const buildCommand = defineCommand({
86
86
  const buildResults = [];
87
87
  for (const file of lambdaFiles) {
88
88
  const name = file.replace(/\.ts$/, "");
89
- const bundleName = getLambdaBundleName(apiName, name);
89
+ const bundleName = getLambdaBundleName(name, name);
90
90
  const inputPath = join(lambdasDir, file);
91
91
  // Create wrapper entry that imports the original and exports handler
92
92
  const wrapperPath = join(tempDir, `${name}-wrapper.ts`);
@@ -112,7 +112,7 @@ export const buildCommand = defineCommand({
112
112
  const packageSizes = new Map();
113
113
  for (const file of lambdaFiles) {
114
114
  const name = file.replace(/\.ts$/, "");
115
- const bundleName = getLambdaBundleName(apiName, name);
115
+ const bundleName = getLambdaBundleName(name, name);
116
116
  const jsPath = join(outputDir, `${bundleName}.js`);
117
117
  const result = await packageLambda(bundleName, jsPath, outputDir);
118
118
  if (!result.success) {
@@ -129,7 +129,7 @@ export const buildCommand = defineCommand({
129
129
  // Generate manifest
130
130
  console.log(` ${pc.green("✓")} Generating manifest...`);
131
131
  try {
132
- const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, apiName);
132
+ const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, name);
133
133
  // Write JSON manifest
134
134
  const manifestPath = join(outputDir, "manifest.json");
135
135
  await writeManifest(manifest, manifestPath);
@@ -145,8 +145,8 @@ export const buildCommand = defineCommand({
145
145
  const routesByLambda = new Map();
146
146
  for (const route of manifest.routes) {
147
147
  // Extract display name (original name) from bundle name
148
- const displayName = route.lambda.startsWith(`${apiName}-`)
149
- ? route.lambda.slice(apiName.length + 1)
148
+ const displayName = route.lambda.startsWith(`${name}-`)
149
+ ? route.lambda.slice(name.length + 1)
150
150
  : route.lambda;
151
151
  const existing = routesByLambda.get(displayName) || [];
152
152
  existing.push(route);
@@ -32,7 +32,7 @@ export const devCommand = defineCommand({
32
32
  consola.error(error instanceof Error ? error.message : error);
33
33
  process.exit(1);
34
34
  }
35
- const apiName = config.apiName;
35
+ const name = config.name;
36
36
  const lambdasDir = join(process.cwd(), config.lambdasDir);
37
37
  const outputDir = join(process.cwd(), config.outputDir);
38
38
  const tempDir = join(outputDir, "__temp__");
@@ -51,7 +51,7 @@ export const devCommand = defineCommand({
51
51
  // Build function for a single lambda
52
52
  async function buildSingleLambda(filename) {
53
53
  const name = filename.replace(/\.ts$/, "");
54
- const bundleName = getLambdaBundleName(apiName, name);
54
+ const bundleName = getLambdaBundleName(name, name);
55
55
  const inputPath = join(lambdasDir, filename);
56
56
  // Create wrapper entry
57
57
  const wrapperPath = join(tempDir, `${name}-wrapper.ts`);
@@ -25,7 +25,7 @@ export const generateIacCommand = defineCommand({
25
25
  consola.error(error instanceof Error ? error.message : error);
26
26
  process.exit(1);
27
27
  }
28
- const apiName = config.apiName;
28
+ const name = config.name;
29
29
  const outputDir = join(process.cwd(), config.outputDir);
30
30
  const terraformDir = join(process.cwd(), config.terraform.outputDir);
31
31
  // Check that build output exists
@@ -45,14 +45,14 @@ export const generateIacCommand = defineCommand({
45
45
  // The manifest generator will re-add the prefix
46
46
  const lambdaFiles = jsFiles.map((f) => {
47
47
  const bundleName = f.replace(/\.js$/, "");
48
- const originalName = getOriginalLambdaName(apiName, bundleName);
48
+ const originalName = getOriginalLambdaName(name, bundleName);
49
49
  return `${originalName}.ts`;
50
50
  });
51
51
  consola.info(`Found ${lambdaFiles.length} built lambda(s)`);
52
52
  // Generate manifest
53
53
  consola.start("Generating route manifest...");
54
54
  try {
55
- const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, apiName);
55
+ const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, name);
56
56
  // Write JSON manifest
57
57
  const manifestPath = join(outputDir, "manifest.json");
58
58
  await writeManifest(manifest, manifestPath);
@@ -24,7 +24,7 @@ export interface ProjectInfo {
24
24
  hasLambdasDir: boolean;
25
25
  /** Whether there are any .ts files in src/lambdas/ */
26
26
  hasLambdaFiles: boolean;
27
- /** Directory name (for default apiName) */
27
+ /** Directory name (for default name) */
28
28
  directoryName: string;
29
29
  }
30
30
  /**
@@ -4,7 +4,7 @@
4
4
  import type { PackageManager } from "./detect";
5
5
  export interface WizardAnswers {
6
6
  targetDir: string;
7
- apiName: string;
7
+ name: string;
8
8
  packageManager: PackageManager;
9
9
  installDeps: boolean;
10
10
  }
@@ -16,8 +16,8 @@ export declare function promptTargetDirectory(currentDirName: string): Promise<s
16
16
  /**
17
17
  * Run wizard for a fresh (empty) project
18
18
  */
19
- export declare function runFreshProjectWizard(apiName: string): Promise<Omit<WizardAnswers, "targetDir" | "apiName"> | null>;
19
+ export declare function runFreshProjectWizard(apiName: string): Promise<Omit<WizardAnswers, "targetDir"> | null>;
20
20
  /**
21
21
  * Run wizard for an existing project
22
22
  */
23
- export declare function runExistingProjectWizard(apiName: string, detectedPackageManager: PackageManager | null): Promise<Omit<WizardAnswers, "targetDir" | "apiName"> | null>;
23
+ export declare function runExistingProjectWizard(apiName: string, detectedPackageManager: PackageManager | null): Promise<Omit<WizardAnswers, "targetDir"> | null>;
@@ -46,6 +46,7 @@ export async function runFreshProjectWizard(apiName) {
46
46
  return null;
47
47
  }
48
48
  return {
49
+ name: apiName,
49
50
  packageManager: packageManager,
50
51
  installDeps: true, // Always install for fresh projects
51
52
  };
@@ -87,6 +88,7 @@ export async function runExistingProjectWizard(apiName, detectedPackageManager)
87
88
  return null;
88
89
  }
89
90
  return {
91
+ name: apiName,
90
92
  packageManager,
91
93
  installDeps: installDeps,
92
94
  };
@@ -49,7 +49,7 @@ export async function scaffoldProject(cwd, info, answers, force) {
49
49
  // For fresh projects, create package.json, .gitignore, tsconfig.json
50
50
  if (info.isEmpty) {
51
51
  const packageJsonPath = join(cwd, "package.json");
52
- await writeFile(packageJsonPath, packageJsonTemplate(answers.apiName), result, false);
52
+ await writeFile(packageJsonPath, packageJsonTemplate(answers.name), result, false);
53
53
  const gitignorePath = join(cwd, ".gitignore");
54
54
  if (!existsSync(gitignorePath)) {
55
55
  await writeFile(gitignorePath, gitignoreTemplate(), result, false);
@@ -62,7 +62,7 @@ export async function scaffoldProject(cwd, info, answers, force) {
62
62
  // Create elysian.config.ts
63
63
  const configPath = join(cwd, "elysian.config.ts");
64
64
  if (!existsSync(configPath) || force) {
65
- await writeFile(configPath, configTemplate(answers.apiName), result, existsSync(configPath));
65
+ await writeFile(configPath, configTemplate(answers.name), result, existsSync(configPath));
66
66
  }
67
67
  else {
68
68
  result.skipped.push(configPath);
@@ -73,13 +73,13 @@ export async function scaffoldProject(cwd, info, answers, force) {
73
73
  await writeFile(exampleLambdaPath, exampleLambdaTemplate(), result, false);
74
74
  }
75
75
  // Handle Terraform files
76
- await scaffoldTerraform(cwd, info, answers.apiName, result);
76
+ await scaffoldTerraform(cwd, info, answers.name, result);
77
77
  return result;
78
78
  }
79
79
  /**
80
80
  * Scaffold Terraform files with smart appending
81
81
  */
82
- async function scaffoldTerraform(cwd, info, apiName, result) {
82
+ async function scaffoldTerraform(cwd, info, name, result) {
83
83
  const tfDir = join(cwd, "terraform");
84
84
  // providers.tf
85
85
  const providersPath = join(tfDir, "providers.tf");
@@ -100,7 +100,7 @@ async function scaffoldTerraform(cwd, info, apiName, result) {
100
100
  const variablesPath = join(tfDir, "variables.tf");
101
101
  if (info.terraformFiles.variables) {
102
102
  const existing = await readFileOrEmpty(variablesPath);
103
- const updated = getMissingVariables(existing, apiName);
103
+ const updated = getMissingVariables(existing, name);
104
104
  if (updated !== existing) {
105
105
  await writeFile(variablesPath, updated, result, true);
106
106
  }
@@ -109,7 +109,7 @@ async function scaffoldTerraform(cwd, info, apiName, result) {
109
109
  }
110
110
  }
111
111
  else {
112
- await writeFile(variablesPath, tfTemplates.variables(apiName), result, false);
112
+ await writeFile(variablesPath, tfTemplates.variables(name), result, false);
113
113
  }
114
114
  // main.tf
115
115
  const mainPath = join(tfDir, "main.tf");
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * elysian.config.ts template
6
6
  */
7
- export declare function configTemplate(apiName: string): string;
7
+ export declare function configTemplate(name: string): string;
8
8
  /**
9
9
  * Example lambda template
10
10
  */
@@ -12,7 +12,7 @@ export declare function exampleLambdaTemplate(): string;
12
12
  /**
13
13
  * package.json template for fresh projects
14
14
  */
15
- export declare function packageJsonTemplate(apiName: string): string;
15
+ export declare function packageJsonTemplate(name: string): string;
16
16
  /**
17
17
  * .gitignore template
18
18
  */
@@ -4,15 +4,11 @@
4
4
  /**
5
5
  * elysian.config.ts template
6
6
  */
7
- export function configTemplate(apiName) {
7
+ export function configTemplate(name) {
8
8
  return `import { defineConfig } from "@actuallyjamez/elysian";
9
9
 
10
10
  export default defineConfig({
11
- apiName: "${apiName}",
12
- openapi: {
13
- title: "${apiName}",
14
- version: "1.0.0",
15
- },
11
+ name: "${name}",
16
12
  });
17
13
  `;
18
14
  }
@@ -40,9 +36,9 @@ export default createLambda()
40
36
  /**
41
37
  * package.json template for fresh projects
42
38
  */
43
- export function packageJsonTemplate(apiName) {
39
+ export function packageJsonTemplate(name) {
44
40
  return JSON.stringify({
45
- name: apiName,
41
+ name: name,
46
42
  version: "0.1.0",
47
43
  type: "module",
48
44
  scripts: {
@@ -24,7 +24,7 @@ export declare function appendProviders(existing: string): string;
24
24
  /**
25
25
  * Get missing variables and return the block to append
26
26
  */
27
- export declare function getMissingVariables(existing: string, apiName: string): string;
27
+ export declare function getMissingVariables(existing: string, name: string): string;
28
28
  /**
29
29
  * Smart append to main.tf - only add if API Gateway resource is missing
30
30
  */
@@ -38,7 +38,7 @@ export declare function appendOutputs(existing: string): string;
38
38
  */
39
39
  export declare const templates: {
40
40
  providers: string;
41
- variables: (apiName: string) => string;
41
+ variables: (name: string) => string;
42
42
  main: string;
43
43
  outputs: string;
44
44
  };
@@ -58,7 +58,7 @@ export function appendProviders(existing) {
58
58
  /**
59
59
  * Get missing variables and return the block to append
60
60
  */
61
- export function getMissingVariables(existing, apiName) {
61
+ export function getMissingVariables(existing, name) {
62
62
  const variables = {
63
63
  region: `
64
64
  variable "region" {
@@ -95,9 +95,9 @@ variable "lambda_timeout" {
95
95
  default = 30
96
96
  }`,
97
97
  api_name: `
98
- variable "api_name" {
98
+ variable "api_name" {
99
99
  type = string
100
- default = "${apiName}"
100
+ default = "${name}"
101
101
  }`,
102
102
  tags: `
103
103
  variable "tags" {
@@ -252,7 +252,7 @@ export function appendOutputs(existing) {
252
252
  */
253
253
  export const templates = {
254
254
  providers: PROVIDERS_BLOCK.trim() + "\n",
255
- variables: (apiName) => `# Elysian: Terraform Variables
255
+ variables: (name) => `# Elysian: Terraform Variables
256
256
 
257
257
  variable "region" {
258
258
  type = string
@@ -290,7 +290,7 @@ variable "lambda_timeout" {
290
290
 
291
291
  variable "api_name" {
292
292
  type = string
293
- default = "${apiName}"
293
+ default = "${name}"
294
294
  }
295
295
 
296
296
  variable "tags" {
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { defineCommand } from "citty";
5
5
  import consola from "consola";
6
+ import pc from "picocolors";
6
7
  import { existsSync, mkdirSync } from "fs";
7
8
  import { resolve, basename } from "path";
8
9
  import { detectProject, } from "./init/detect";
@@ -30,7 +31,7 @@ export const initCommand = defineCommand({
30
31
  }
31
32
  // Resolve the target directory
32
33
  const cwd = resolve(originalCwd, targetDir);
33
- const apiName = basename(cwd);
34
+ const name = basename(cwd);
34
35
  // Create directory if it doesn't exist
35
36
  if (!existsSync(cwd)) {
36
37
  mkdirSync(cwd, { recursive: true });
@@ -46,26 +47,24 @@ export const initCommand = defineCommand({
46
47
  // Run appropriate wizard based on whether directory is empty
47
48
  let answers;
48
49
  if (info.isEmpty) {
49
- const result = await runFreshProjectWizard(apiName);
50
+ const result = await runFreshProjectWizard(name);
50
51
  if (!result) {
51
52
  consola.info("Cancelled");
52
53
  process.exit(0);
53
54
  }
54
55
  answers = {
55
56
  targetDir,
56
- apiName,
57
57
  ...result,
58
58
  };
59
59
  }
60
60
  else {
61
- const result = await runExistingProjectWizard(apiName, info.packageManager);
61
+ const result = await runExistingProjectWizard(name, info.packageManager);
62
62
  if (!result) {
63
63
  consola.info("Cancelled");
64
64
  process.exit(0);
65
65
  }
66
66
  answers = {
67
67
  targetDir,
68
- apiName,
69
68
  ...result,
70
69
  };
71
70
  }
@@ -89,11 +88,19 @@ export const initCommand = defineCommand({
89
88
  const runCmd = pm === "npm" ? "npm run" : pm;
90
89
  // If we created in a subdirectory, tell user to cd into it
91
90
  const cdStep = targetDir !== "." ? `cd ${targetDir}\n\n` : "";
92
- consola.box("Project initialized!\n\n" +
93
- "Next steps:\n\n" +
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");
91
+ console.log(` ${pc.green("✓")} Project initialized!`);
92
+ console.log();
93
+ console.log(` ${pc.bold("Next steps")}:`);
94
+ console.log();
95
+ if (cdStep) {
96
+ console.log(` ${cdStep}`);
97
+ }
98
+ if (!answers.installDeps) {
99
+ console.log(` ${pm} add elysia @actuallyjamez/elysian`);
100
+ console.log();
101
+ }
102
+ console.log(` ${runCmd} elysian build`);
103
+ console.log();
104
+ console.log(` cd terraform && terraform init && terraform apply`);
98
105
  },
99
106
  });
@@ -4,9 +4,9 @@
4
4
  export interface OpenAPIConfig {
5
5
  /** Enable OpenAPI auto-aggregation (default: true) */
6
6
  enabled?: boolean;
7
- /** API title for OpenAPI spec */
7
+ /** API title for OpenAPI spec (defaults to name if not provided) */
8
8
  title?: string;
9
- /** API version for OpenAPI spec */
9
+ /** API version for OpenAPI spec (defaults to package.json version if not provided) */
10
10
  version?: string;
11
11
  /** API description for OpenAPI spec */
12
12
  description?: string;
@@ -35,7 +35,7 @@ export interface TerraformConfig {
35
35
  }
36
36
  export interface ElysianConfig {
37
37
  /** Name of the API (used for resource naming) */
38
- apiName: string;
38
+ name: string;
39
39
  /** Directory containing lambda files (default: "src/lambdas") */
40
40
  lambdasDir?: string;
41
41
  /** Output directory for built lambdas (default: "dist") */
@@ -50,7 +50,7 @@ export interface ElysianConfig {
50
50
  terraform?: TerraformConfig;
51
51
  }
52
52
  export interface ResolvedConfig {
53
- apiName: string;
53
+ name: string;
54
54
  lambdasDir: string;
55
55
  outputDir: string;
56
56
  openapi: Required<OpenAPIConfig>;
@@ -65,7 +65,7 @@ export declare function defineConfig(config: ElysianConfig): ElysianConfig;
65
65
  /**
66
66
  * Resolve configuration with defaults applied
67
67
  */
68
- export declare function resolveConfig(config: ElysianConfig): ResolvedConfig;
68
+ export declare function resolveConfig(config: ElysianConfig, cwd: string): Promise<ResolvedConfig>;
69
69
  /**
70
70
  * Load configuration from elysian.config.ts
71
71
  */
@@ -25,6 +25,19 @@ const DEFAULT_CONFIG = {
25
25
  tfvarsFilename: "api-routes.auto.tfvars",
26
26
  },
27
27
  };
28
+ /**
29
+ * Read version from package.json
30
+ */
31
+ async function readPackageVersion(cwd) {
32
+ const packagePath = `${cwd}/package.json`;
33
+ try {
34
+ const content = await Bun.file(packagePath).json();
35
+ return content.version || null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
28
41
  /**
29
42
  * Define configuration with type safety and defaults
30
43
  */
@@ -34,15 +47,16 @@ export function defineConfig(config) {
34
47
  /**
35
48
  * Resolve configuration with defaults applied
36
49
  */
37
- export function resolveConfig(config) {
50
+ export async function resolveConfig(config, cwd) {
51
+ const pkgVersion = await readPackageVersion(cwd);
38
52
  return {
39
- apiName: config.apiName,
53
+ name: config.name,
40
54
  lambdasDir: config.lambdasDir ?? DEFAULT_CONFIG.lambdasDir,
41
55
  outputDir: config.outputDir ?? DEFAULT_CONFIG.outputDir,
42
56
  openapi: {
43
57
  enabled: config.openapi?.enabled ?? DEFAULT_CONFIG.openapi.enabled,
44
- title: config.openapi?.title ?? DEFAULT_CONFIG.openapi.title,
45
- version: config.openapi?.version ?? DEFAULT_CONFIG.openapi.version,
58
+ title: config.openapi?.title ?? config.name,
59
+ version: config.openapi?.version ?? pkgVersion ?? DEFAULT_CONFIG.openapi.version,
46
60
  description: config.openapi?.description ?? DEFAULT_CONFIG.openapi.description,
47
61
  },
48
62
  build: {
@@ -70,10 +84,10 @@ export async function loadConfig(cwd = process.cwd()) {
70
84
  try {
71
85
  const configModule = await import(configPath);
72
86
  const config = configModule.default;
73
- if (!config.apiName) {
74
- throw new Error("apiName is required in elysian.config.ts");
87
+ if (!config.name) {
88
+ throw new Error("name is required in elysian.config.ts");
75
89
  }
76
- return resolveConfig(config);
90
+ return resolveConfig(config, cwd);
77
91
  }
78
92
  catch (error) {
79
93
  if (error.code === "ERR_MODULE_NOT_FOUND") {
@@ -36,7 +36,7 @@ export declare function generateRouteName(lambda: string, method: string, path:
36
36
  /**
37
37
  * Generate manifest by introspecting built lambda modules
38
38
  */
39
- export declare function generateManifest(lambdaFiles: string[], outputDir: string, openapiEnabled?: boolean, apiName?: string): Promise<ApiManifest>;
39
+ export declare function generateManifest(lambdaFiles: string[], outputDir: string, openapiEnabled?: boolean, name?: string): Promise<ApiManifest>;
40
40
  /**
41
41
  * Write manifest to JSON file
42
42
  */
@@ -31,7 +31,7 @@ export function generateRouteName(lambda, method, path) {
31
31
  /**
32
32
  * Generate manifest by introspecting built lambda modules
33
33
  */
34
- export async function generateManifest(lambdaFiles, outputDir, openapiEnabled = true, apiName = "") {
34
+ export async function generateManifest(lambdaFiles, outputDir, openapiEnabled = true, name = "") {
35
35
  const manifest = {
36
36
  lambdas: [],
37
37
  routes: [],
@@ -43,7 +43,7 @@ export async function generateManifest(lambdaFiles, outputDir, openapiEnabled =
43
43
  for (const file of sortedFiles) {
44
44
  const originalName = file.replace(/\.ts$/, "");
45
45
  // Use prefixed bundle name for file lookup
46
- const bundleName = apiName ? getLambdaBundleName(apiName, originalName) : originalName;
46
+ const bundleName = name ? getLambdaBundleName(name, originalName) : originalName;
47
47
  const modulePath = isAbsolute(outputDir)
48
48
  ? join(outputDir, `${bundleName}.js`)
49
49
  : join(process.cwd(), outputDir, `${bundleName}.js`);
@@ -71,12 +71,12 @@ export async function generateManifest(lambdaFiles, outputDir, openapiEnabled =
71
71
  });
72
72
  // Determine which lambda should handle this route
73
73
  let targetLambda = bundleName;
74
- // OpenAPI routes always go to the __openapi__ lambda if enabled
74
+ // OpenAPI routes always go to __openapi__ lambda if enabled
75
75
  if (openapiEnabled && path.startsWith("/openapi")) {
76
- targetLambda = apiName ? getLambdaBundleName(apiName, "__openapi__") : "__openapi__";
76
+ targetLambda = name ? getLambdaBundleName(name, "__openapi__") : "__openapi__";
77
77
  }
78
78
  else if (originalName === "__openapi__") {
79
- // Skip non-openapi routes from the openapi aggregator lambda
79
+ // Skip non-openapi routes from openapi aggregator lambda
80
80
  continue;
81
81
  }
82
82
  // Check for route conflicts
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Naming utilities for lambda bundles
2
+ * Lambda naming utilities
3
3
  */
4
4
  /**
5
- * Generate the prefixed lambda name for bundle files
6
- * Format: {apiName}-{lambdaName}
5
+ * Generate Lambda bundle name with API name prefix
6
+ * Format: {name}-{lambdaName}
7
7
  */
8
- export declare function getLambdaBundleName(apiName: string, lambdaName: string): string;
8
+ export declare function getLambdaBundleName(name: string, lambdaName: string): string;
9
9
  /**
10
- * Extract the original lambda name from a prefixed bundle name
10
+ * Extract original lambda name from bundle name
11
+ * Reverses getLambdaBundleName()
11
12
  */
12
- export declare function getOriginalLambdaName(apiName: string, bundleName: string): string;
13
+ export declare function getOriginalLambdaName(name: string, bundleName: string): string;
@@ -1,18 +1,19 @@
1
1
  /**
2
- * Naming utilities for lambda bundles
2
+ * Lambda naming utilities
3
3
  */
4
4
  /**
5
- * Generate the prefixed lambda name for bundle files
6
- * Format: {apiName}-{lambdaName}
5
+ * Generate Lambda bundle name with API name prefix
6
+ * Format: {name}-{lambdaName}
7
7
  */
8
- export function getLambdaBundleName(apiName, lambdaName) {
9
- return `${apiName}-${lambdaName}`;
8
+ export function getLambdaBundleName(name, lambdaName) {
9
+ return `${name}-${lambdaName}`;
10
10
  }
11
11
  /**
12
- * Extract the original lambda name from a prefixed bundle name
12
+ * Extract original lambda name from bundle name
13
+ * Reverses getLambdaBundleName()
13
14
  */
14
- export function getOriginalLambdaName(apiName, bundleName) {
15
- const prefix = `${apiName}-`;
15
+ export function getOriginalLambdaName(name, bundleName) {
16
+ const prefix = `${name}-`;
16
17
  if (bundleName.startsWith(prefix)) {
17
18
  return bundleName.slice(prefix.length);
18
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuallyjamez/elysian",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Automatic Lambda bundler for Elysia with API Gateway and Terraform integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",