@actuallyjamez/elysian 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 apiName) */
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
+ apiName: 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" | "apiName"> | null>;
20
+ /**
21
+ * Run wizard for an existing project
22
+ */
23
+ export declare function runExistingProjectWizard(apiName: string, detectedPackageManager: PackageManager | null): Promise<Omit<WizardAnswers, "targetDir" | "apiName"> | null>;
@@ -0,0 +1,93 @@
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
+ packageManager: packageManager,
50
+ installDeps: true, // Always install for fresh projects
51
+ };
52
+ }
53
+ /**
54
+ * Run wizard for an existing project
55
+ */
56
+ export async function runExistingProjectWizard(apiName, detectedPackageManager) {
57
+ consola.info(`Adding elysian to: ${apiName}\n`);
58
+ // Use detected package manager or prompt
59
+ let packageManager;
60
+ if (detectedPackageManager) {
61
+ packageManager = detectedPackageManager;
62
+ consola.info(`Detected package manager: ${packageManager}`);
63
+ }
64
+ else {
65
+ const selected = await consola.prompt("Package manager:", {
66
+ type: "select",
67
+ options: [
68
+ { value: "bun", label: "bun", hint: "recommended" },
69
+ { value: "npm", label: "npm" },
70
+ { value: "pnpm", label: "pnpm" },
71
+ { value: "yarn", label: "yarn" },
72
+ ],
73
+ cancel: "symbol",
74
+ });
75
+ if (isCancelled(selected)) {
76
+ return null;
77
+ }
78
+ packageManager = selected;
79
+ }
80
+ // Prompt whether to install dependencies
81
+ const installDeps = await consola.prompt("Install dependencies?", {
82
+ type: "confirm",
83
+ initial: true,
84
+ cancel: "symbol",
85
+ });
86
+ if (isCancelled(installDeps)) {
87
+ return null;
88
+ }
89
+ return {
90
+ packageManager,
91
+ installDeps: installDeps,
92
+ };
93
+ }
@@ -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.apiName), 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.apiName), 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.apiName, result);
77
+ return result;
78
+ }
79
+ /**
80
+ * Scaffold Terraform files with smart appending
81
+ */
82
+ async function scaffoldTerraform(cwd, info, apiName, 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, apiName);
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(apiName), 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(apiName: 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(apiName: 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,103 @@
1
+ /**
2
+ * Template strings for generated files
3
+ */
4
+ /**
5
+ * elysian.config.ts template
6
+ */
7
+ export function configTemplate(apiName) {
8
+ return `import { defineConfig } from "@actuallyjamez/elysian";
9
+
10
+ export default defineConfig({
11
+ apiName: "${apiName}",
12
+ openapi: {
13
+ title: "${apiName}",
14
+ version: "1.0.0",
15
+ },
16
+ });
17
+ `;
18
+ }
19
+ /**
20
+ * Example lambda template
21
+ */
22
+ export function exampleLambdaTemplate() {
23
+ return `import { createLambda, t } from "@actuallyjamez/elysian";
24
+
25
+ export default createLambda()
26
+ .get("/hello", ({ query }) => {
27
+ return \`Hello, \${query.name ?? "World"}!\`;
28
+ }, {
29
+ response: t.String(),
30
+ query: t.Object({
31
+ name: t.Optional(t.String()),
32
+ }),
33
+ detail: {
34
+ summary: "Say hello",
35
+ tags: ["Greeting"],
36
+ },
37
+ });
38
+ `;
39
+ }
40
+ /**
41
+ * package.json template for fresh projects
42
+ */
43
+ export function packageJsonTemplate(apiName) {
44
+ return JSON.stringify({
45
+ name: apiName,
46
+ version: "0.1.0",
47
+ type: "module",
48
+ scripts: {
49
+ build: "elysian build",
50
+ dev: "elysian dev",
51
+ },
52
+ }, null, 2);
53
+ }
54
+ /**
55
+ * .gitignore template
56
+ */
57
+ export function gitignoreTemplate() {
58
+ return `# Dependencies
59
+ node_modules/
60
+
61
+ # Build output
62
+ dist/
63
+
64
+ # Environment
65
+ .env
66
+ .env.local
67
+
68
+ # Terraform
69
+ terraform/.terraform/
70
+ terraform/*.tfstate
71
+ terraform/*.tfstate.backup
72
+ terraform/.terraform.lock.hcl
73
+
74
+ # IDE
75
+ .idea/
76
+ .vscode/
77
+ *.swp
78
+ *.swo
79
+
80
+ # OS
81
+ .DS_Store
82
+ Thumbs.db
83
+ `;
84
+ }
85
+ /**
86
+ * tsconfig.json template
87
+ */
88
+ export function tsconfigTemplate() {
89
+ return JSON.stringify({
90
+ compilerOptions: {
91
+ target: "ESNext",
92
+ module: "ESNext",
93
+ moduleResolution: "bundler",
94
+ strict: true,
95
+ esModuleInterop: true,
96
+ skipLibCheck: true,
97
+ noEmit: true,
98
+ types: ["bun-types"],
99
+ },
100
+ include: ["src/**/*", "elysian.config.ts"],
101
+ exclude: ["node_modules", "dist"],
102
+ }, null, 2);
103
+ }
@@ -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, apiName: 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: (apiName: string) => string;
42
+ main: string;
43
+ outputs: string;
44
+ };