@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 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);
@@ -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,6 @@
1
+ /**
2
+ * Init module exports
3
+ */
4
+ export { detectProject } from "./detect";
5
+ export { promptTargetDirectory, runFreshProjectWizard, runExistingProjectWizard, } from "./prompts";
6
+ export { scaffoldProject, installDependencies, printResults, } 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
+ }