@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
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
|
|
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
|
-
#
|
|
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
|
-
|
|
36
|
+
# Interactive wizard - prompts for everything you need
|
|
37
|
+
elysian init
|
|
29
38
|
```
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
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 -
|
|
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
|
-
|
|
99
|
+
elysian build
|
|
64
100
|
|
|
65
101
|
# Production build (minified)
|
|
66
|
-
|
|
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
|
-
|
|
121
|
+
// Required: Used for naming your AWS resources
|
|
122
|
+
name: "my-api",
|
|
87
123
|
|
|
88
|
-
// Optional (
|
|
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
|
|
96
|
-
version
|
|
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
|
|
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
|
|
144
|
+
// Optional: Lambda defaults
|
|
107
145
|
lambda: {
|
|
108
|
-
runtime: "
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
```
|
|
210
|
+
# Watch with packaging
|
|
211
|
+
elysian dev
|
|
140
212
|
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
206
|
-
│ ├── users.js
|
|
207
|
-
│ ├── users.zip
|
|
208
|
-
│ ├── posts.js
|
|
209
|
-
│ ├── posts.zip
|
|
210
|
-
│ ├── __openapi__.js
|
|
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
|
-
├──
|
|
215
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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(`${
|
|
149
|
-
? route.lambda.slice(
|
|
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);
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection utilities for init wizard
|
|
3
|
+
*/
|
|
4
|
+
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
5
|
+
export interface ProjectInfo {
|
|
6
|
+
/** Whether the directory is empty (only hidden files allowed) */
|
|
7
|
+
isEmpty: boolean;
|
|
8
|
+
/** Detected package manager from lockfile, or null */
|
|
9
|
+
packageManager: PackageManager | null;
|
|
10
|
+
/** Name from package.json, or null */
|
|
11
|
+
packageName: string | null;
|
|
12
|
+
/** Whether elysian.config.ts exists */
|
|
13
|
+
hasElysianConfig: boolean;
|
|
14
|
+
/** Whether terraform/ directory exists */
|
|
15
|
+
hasTerraformDir: boolean;
|
|
16
|
+
/** Existing terraform files */
|
|
17
|
+
terraformFiles: {
|
|
18
|
+
providers: boolean;
|
|
19
|
+
variables: boolean;
|
|
20
|
+
main: boolean;
|
|
21
|
+
outputs: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** Whether src/lambdas/ exists */
|
|
24
|
+
hasLambdasDir: boolean;
|
|
25
|
+
/** Whether there are any .ts files in src/lambdas/ */
|
|
26
|
+
hasLambdaFiles: boolean;
|
|
27
|
+
/** Directory name (for default name) */
|
|
28
|
+
directoryName: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Gather all project information for the init wizard
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectProject(cwd: string): Promise<ProjectInfo>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection utilities for init wizard
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
import { join, basename } from "path";
|
|
6
|
+
/**
|
|
7
|
+
* Check if a directory is empty (ignoring hidden files like .git)
|
|
8
|
+
*/
|
|
9
|
+
function isDirectoryEmpty(cwd) {
|
|
10
|
+
try {
|
|
11
|
+
const files = readdirSync(cwd);
|
|
12
|
+
// Filter out hidden files (starting with .)
|
|
13
|
+
const visibleFiles = files.filter((f) => !f.startsWith("."));
|
|
14
|
+
return visibleFiles.length === 0;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Detect package manager from lockfiles
|
|
22
|
+
*/
|
|
23
|
+
function detectPackageManager(cwd) {
|
|
24
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
|
|
25
|
+
return "bun";
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
28
|
+
return "pnpm";
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
31
|
+
return "yarn";
|
|
32
|
+
}
|
|
33
|
+
if (existsSync(join(cwd, "package-lock.json"))) {
|
|
34
|
+
return "npm";
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read package name from package.json
|
|
40
|
+
*/
|
|
41
|
+
async function readPackageName(cwd) {
|
|
42
|
+
const packagePath = join(cwd, "package.json");
|
|
43
|
+
if (!existsSync(packagePath)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const content = await Bun.file(packagePath).json();
|
|
48
|
+
return content.name || null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check which terraform files exist
|
|
56
|
+
*/
|
|
57
|
+
function checkTerraformFiles(cwd) {
|
|
58
|
+
const tfDir = join(cwd, "terraform");
|
|
59
|
+
return {
|
|
60
|
+
providers: existsSync(join(tfDir, "providers.tf")),
|
|
61
|
+
variables: existsSync(join(tfDir, "variables.tf")),
|
|
62
|
+
main: existsSync(join(tfDir, "main.tf")),
|
|
63
|
+
outputs: existsSync(join(tfDir, "outputs.tf")),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if there are any .ts files in the lambdas directory
|
|
68
|
+
*/
|
|
69
|
+
function hasLambdaFiles(cwd) {
|
|
70
|
+
const lambdasDir = join(cwd, "src/lambdas");
|
|
71
|
+
if (!existsSync(lambdasDir)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const files = readdirSync(lambdasDir);
|
|
76
|
+
return files.some((f) => f.endsWith(".ts"));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Gather all project information for the init wizard
|
|
84
|
+
*/
|
|
85
|
+
export async function detectProject(cwd) {
|
|
86
|
+
const packageName = await readPackageName(cwd);
|
|
87
|
+
return {
|
|
88
|
+
isEmpty: isDirectoryEmpty(cwd),
|
|
89
|
+
packageManager: detectPackageManager(cwd),
|
|
90
|
+
packageName,
|
|
91
|
+
hasElysianConfig: existsSync(join(cwd, "elysian.config.ts")),
|
|
92
|
+
hasTerraformDir: existsSync(join(cwd, "terraform")),
|
|
93
|
+
terraformFiles: checkTerraformFiles(cwd),
|
|
94
|
+
hasLambdasDir: existsSync(join(cwd, "src/lambdas")),
|
|
95
|
+
hasLambdaFiles: hasLambdaFiles(cwd),
|
|
96
|
+
directoryName: basename(cwd),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init module exports
|
|
3
|
+
*/
|
|
4
|
+
export { detectProject, type ProjectInfo, type PackageManager } from "./detect";
|
|
5
|
+
export { promptTargetDirectory, runFreshProjectWizard, runExistingProjectWizard, type WizardAnswers, } from "./prompts";
|
|
6
|
+
export { scaffoldProject, installDependencies, printResults, type ScaffoldResult, } from "./scaffold";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard prompts using consola.prompt
|
|
3
|
+
*/
|
|
4
|
+
import type { PackageManager } from "./detect";
|
|
5
|
+
export interface WizardAnswers {
|
|
6
|
+
targetDir: string;
|
|
7
|
+
name: string;
|
|
8
|
+
packageManager: PackageManager;
|
|
9
|
+
installDeps: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run the initial wizard to get the target directory
|
|
13
|
+
* Returns the target directory path, or null if cancelled
|
|
14
|
+
*/
|
|
15
|
+
export declare function promptTargetDirectory(currentDirName: string): Promise<string | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Run wizard for a fresh (empty) project
|
|
18
|
+
*/
|
|
19
|
+
export declare function runFreshProjectWizard(apiName: string): Promise<Omit<WizardAnswers, "targetDir"> | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Run wizard for an existing project
|
|
22
|
+
*/
|
|
23
|
+
export declare function runExistingProjectWizard(apiName: string, detectedPackageManager: PackageManager | null): Promise<Omit<WizardAnswers, "targetDir"> | null>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard prompts using consola.prompt
|
|
3
|
+
*/
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
const CANCEL_SYMBOL = Symbol.for("cancel");
|
|
6
|
+
/**
|
|
7
|
+
* Check if user cancelled the prompt
|
|
8
|
+
*/
|
|
9
|
+
function isCancelled(value) {
|
|
10
|
+
return value === CANCEL_SYMBOL;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run the initial wizard to get the target directory
|
|
14
|
+
* Returns the target directory path, or null if cancelled
|
|
15
|
+
*/
|
|
16
|
+
export async function promptTargetDirectory(currentDirName) {
|
|
17
|
+
console.log("");
|
|
18
|
+
const targetDir = await consola.prompt("Where would you like to create your project?", {
|
|
19
|
+
type: "text",
|
|
20
|
+
default: ".",
|
|
21
|
+
placeholder: ". (current directory)",
|
|
22
|
+
cancel: "symbol",
|
|
23
|
+
});
|
|
24
|
+
if (isCancelled(targetDir)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return targetDir;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run wizard for a fresh (empty) project
|
|
31
|
+
*/
|
|
32
|
+
export async function runFreshProjectWizard(apiName) {
|
|
33
|
+
consola.info(`Creating new elysian project: ${apiName}\n`);
|
|
34
|
+
// Prompt for package manager
|
|
35
|
+
const packageManager = await consola.prompt("Package manager:", {
|
|
36
|
+
type: "select",
|
|
37
|
+
options: [
|
|
38
|
+
{ value: "bun", label: "bun", hint: "recommended" },
|
|
39
|
+
{ value: "npm", label: "npm" },
|
|
40
|
+
{ value: "pnpm", label: "pnpm" },
|
|
41
|
+
{ value: "yarn", label: "yarn" },
|
|
42
|
+
],
|
|
43
|
+
cancel: "symbol",
|
|
44
|
+
});
|
|
45
|
+
if (isCancelled(packageManager)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: apiName,
|
|
50
|
+
packageManager: packageManager,
|
|
51
|
+
installDeps: true, // Always install for fresh projects
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run wizard for an existing project
|
|
56
|
+
*/
|
|
57
|
+
export async function runExistingProjectWizard(apiName, detectedPackageManager) {
|
|
58
|
+
consola.info(`Adding elysian to: ${apiName}\n`);
|
|
59
|
+
// Use detected package manager or prompt
|
|
60
|
+
let packageManager;
|
|
61
|
+
if (detectedPackageManager) {
|
|
62
|
+
packageManager = detectedPackageManager;
|
|
63
|
+
consola.info(`Detected package manager: ${packageManager}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const selected = await consola.prompt("Package manager:", {
|
|
67
|
+
type: "select",
|
|
68
|
+
options: [
|
|
69
|
+
{ value: "bun", label: "bun", hint: "recommended" },
|
|
70
|
+
{ value: "npm", label: "npm" },
|
|
71
|
+
{ value: "pnpm", label: "pnpm" },
|
|
72
|
+
{ value: "yarn", label: "yarn" },
|
|
73
|
+
],
|
|
74
|
+
cancel: "symbol",
|
|
75
|
+
});
|
|
76
|
+
if (isCancelled(selected)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
packageManager = selected;
|
|
80
|
+
}
|
|
81
|
+
// Prompt whether to install dependencies
|
|
82
|
+
const installDeps = await consola.prompt("Install dependencies?", {
|
|
83
|
+
type: "confirm",
|
|
84
|
+
initial: true,
|
|
85
|
+
cancel: "symbol",
|
|
86
|
+
});
|
|
87
|
+
if (isCancelled(installDeps)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
name: apiName,
|
|
92
|
+
packageManager,
|
|
93
|
+
installDeps: installDeps,
|
|
94
|
+
};
|
|
95
|
+
}
|