@actuallyjamez/elysian 0.5.1 → 0.7.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/dist/cli/commands/build.js +21 -62
- package/dist/cli/commands/dev.d.ts +6 -1
- package/dist/cli/commands/dev.js +303 -38
- package/dist/cli/commands/generate-iac.js +41 -23
- package/dist/cli/commands/init/scaffold.js +6 -6
- package/dist/cli/commands/init/templates.js +2 -2
- package/dist/cli/commands/init.js +19 -19
- package/dist/cli/ui.d.ts +114 -0
- package/dist/cli/ui.js +233 -0
- package/dist/core/localstack.d.ts +59 -0
- package/dist/core/localstack.js +168 -0
- package/package.json +3 -2
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Generate IAC command - Regenerate Terraform files without rebuilding
|
|
3
3
|
*/
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import consola from "consola";
|
|
6
5
|
import { readdirSync, existsSync, mkdirSync } from "fs";
|
|
7
6
|
import { join } from "path";
|
|
8
7
|
import { loadConfig } from "../../core/config";
|
|
9
8
|
import { generateManifest, writeManifest } from "../../core/manifest";
|
|
10
9
|
import { writeTerraformVars } from "../../core/terraform";
|
|
11
10
|
import { getOriginalLambdaName } from "../../core/naming";
|
|
11
|
+
import { ui, pc, formatDuration } from "../ui";
|
|
12
12
|
export const generateIacCommand = defineCommand({
|
|
13
13
|
meta: {
|
|
14
14
|
name: "generate-iac",
|
|
@@ -16,13 +16,15 @@ export const generateIacCommand = defineCommand({
|
|
|
16
16
|
},
|
|
17
17
|
args: {},
|
|
18
18
|
async run() {
|
|
19
|
-
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
ui.header();
|
|
21
|
+
// Load config
|
|
20
22
|
let config;
|
|
21
23
|
try {
|
|
22
24
|
config = await loadConfig();
|
|
23
25
|
}
|
|
24
26
|
catch (error) {
|
|
25
|
-
|
|
27
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
26
28
|
process.exit(1);
|
|
27
29
|
}
|
|
28
30
|
const name = config.name;
|
|
@@ -30,7 +32,8 @@ export const generateIacCommand = defineCommand({
|
|
|
30
32
|
const terraformDir = join(process.cwd(), config.terraform.outputDir);
|
|
31
33
|
// Check that build output exists
|
|
32
34
|
if (!existsSync(outputDir)) {
|
|
33
|
-
|
|
35
|
+
ui.error(`Build output directory not found: ${outputDir}`);
|
|
36
|
+
ui.info("Run 'elysian build' first");
|
|
34
37
|
process.exit(1);
|
|
35
38
|
}
|
|
36
39
|
// Ensure terraform directory exists
|
|
@@ -38,7 +41,8 @@ export const generateIacCommand = defineCommand({
|
|
|
38
41
|
// Get built lambda files (they have the apiName prefix)
|
|
39
42
|
const jsFiles = readdirSync(outputDir).filter((f) => f.endsWith(".js") && !f.startsWith("__temp__"));
|
|
40
43
|
if (jsFiles.length === 0) {
|
|
41
|
-
|
|
44
|
+
ui.error(`No built lambda files found in ${outputDir}`);
|
|
45
|
+
ui.info("Run 'elysian build' first");
|
|
42
46
|
process.exit(1);
|
|
43
47
|
}
|
|
44
48
|
// Convert bundle names back to .ts names for manifest generation
|
|
@@ -48,35 +52,49 @@ export const generateIacCommand = defineCommand({
|
|
|
48
52
|
const originalName = getOriginalLambdaName(name, bundleName);
|
|
49
53
|
return `${originalName}.ts`;
|
|
50
54
|
});
|
|
51
|
-
|
|
55
|
+
ui.success(`Found ${lambdaFiles.length} built lambda${lambdaFiles.length === 1 ? "" : "s"}`);
|
|
52
56
|
// Generate manifest
|
|
53
|
-
|
|
57
|
+
ui.info("Generating route manifest...");
|
|
54
58
|
try {
|
|
55
59
|
const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, name);
|
|
56
60
|
// Write JSON manifest
|
|
57
61
|
const manifestPath = join(outputDir, "manifest.json");
|
|
58
62
|
await writeManifest(manifest, manifestPath);
|
|
59
|
-
|
|
63
|
+
ui.success("Generated manifest.json");
|
|
60
64
|
// Write Terraform variables
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`Routes: ${manifest.routes.length}\n\n` +
|
|
68
|
-
`Output: ${config.terraform.outputDir}/${config.terraform.tfvarsFilename}`);
|
|
69
|
-
// Print route summary
|
|
70
|
-
console.log("\nRoute Summary:");
|
|
65
|
+
await writeTerraformVars(manifest, config);
|
|
66
|
+
ui.success(`Generated ${config.terraform.tfvarsFilename}`);
|
|
67
|
+
// Route table
|
|
68
|
+
ui.section("Routes");
|
|
69
|
+
// Group routes by lambda
|
|
70
|
+
const routesByLambda = new Map();
|
|
71
71
|
for (const route of manifest.routes) {
|
|
72
|
-
const
|
|
73
|
-
?
|
|
74
|
-
:
|
|
75
|
-
|
|
72
|
+
const displayName = route.lambda.startsWith(`${name}-`)
|
|
73
|
+
? route.lambda.slice(name.length + 1)
|
|
74
|
+
: route.lambda;
|
|
75
|
+
const existing = routesByLambda.get(displayName) || [];
|
|
76
|
+
existing.push(route);
|
|
77
|
+
routesByLambda.set(displayName, existing);
|
|
76
78
|
}
|
|
79
|
+
// Find longest path for alignment
|
|
80
|
+
const maxPathLen = Math.max(...manifest.routes.map((r) => r.path.length));
|
|
81
|
+
for (const [displayName, routes] of routesByLambda) {
|
|
82
|
+
ui.labeled(displayName);
|
|
83
|
+
for (const route of routes) {
|
|
84
|
+
ui.route(route.method, route.path, route.pathParameters, maxPathLen);
|
|
85
|
+
}
|
|
86
|
+
ui.blank();
|
|
87
|
+
}
|
|
88
|
+
// Summary
|
|
89
|
+
ui.divider();
|
|
90
|
+
const duration = Date.now() - startTime;
|
|
91
|
+
ui.success(`Generated infrastructure for ${pc.bold(String(manifest.lambdas.length))} lambdas (${manifest.routes.length} routes) in ${pc.bold(formatDuration(duration))}`);
|
|
92
|
+
ui.blank();
|
|
93
|
+
ui.keyValue("Output", `${config.terraform.outputDir}/${config.terraform.tfvarsFilename}`);
|
|
94
|
+
ui.blank();
|
|
77
95
|
}
|
|
78
96
|
catch (error) {
|
|
79
|
-
|
|
97
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
80
98
|
process.exit(1);
|
|
81
99
|
}
|
|
82
100
|
},
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { existsSync, mkdirSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { spawn } from "bun";
|
|
7
|
-
import consola from "consola";
|
|
8
7
|
import { configTemplate, exampleLambdaTemplate, packageJsonTemplate, gitignoreTemplate, tsconfigTemplate, } from "./templates";
|
|
9
8
|
import { templates as tfTemplates, appendProviders, getMissingVariables, appendMain, appendOutputs, } from "./terraform";
|
|
9
|
+
import { ui } from "../../ui";
|
|
10
10
|
/**
|
|
11
11
|
* Read file content or return empty string if not exists
|
|
12
12
|
*/
|
|
@@ -150,7 +150,7 @@ export async function installDependencies(cwd, packageManager) {
|
|
|
150
150
|
const devDeps = ["@types/bun", "typescript"];
|
|
151
151
|
const addCmd = packageManager === "npm" ? "install" : "add";
|
|
152
152
|
const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
|
|
153
|
-
|
|
153
|
+
ui.info("Installing dependencies...");
|
|
154
154
|
// Install main dependencies
|
|
155
155
|
const addProc = spawn([packageManager, addCmd, ...deps], {
|
|
156
156
|
cwd,
|
|
@@ -173,7 +173,7 @@ export async function installDependencies(cwd, packageManager) {
|
|
|
173
173
|
const stderr = await new Response(devProc.stderr).text();
|
|
174
174
|
throw new Error(`Failed to install dev dependencies: ${stderr}`);
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
ui.success("Dependencies installed");
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* Get relative path for display
|
|
@@ -185,12 +185,12 @@ export function getRelativePath(cwd, fullPath) {
|
|
|
185
185
|
* Print scaffold results
|
|
186
186
|
*/
|
|
187
187
|
export function printResults(cwd, result) {
|
|
188
|
-
|
|
188
|
+
ui.blank();
|
|
189
189
|
for (const path of result.created) {
|
|
190
|
-
|
|
190
|
+
ui.success(`Created ${getRelativePath(cwd, path)}`);
|
|
191
191
|
}
|
|
192
192
|
for (const path of result.updated) {
|
|
193
|
-
|
|
193
|
+
ui.success(`Updated ${getRelativePath(cwd, path)}`);
|
|
194
194
|
}
|
|
195
195
|
// Don't print skipped files - too noisy
|
|
196
196
|
}
|
|
@@ -19,8 +19,8 @@ export function exampleLambdaTemplate() {
|
|
|
19
19
|
return `import { createLambda, t } from "@actuallyjamez/elysian";
|
|
20
20
|
|
|
21
21
|
export default createLambda()
|
|
22
|
-
.get("/
|
|
23
|
-
return \`Hello, \${query.name ?? "
|
|
22
|
+
.get("/", ({ query }) => {
|
|
23
|
+
return \`Hello, \${query.name ?? "Elysian"}!\`;
|
|
24
24
|
}, {
|
|
25
25
|
response: t.String(),
|
|
26
26
|
query: t.Object({
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Init command - Interactive wizard to initialize elysian projects
|
|
3
3
|
*/
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import consola from "consola";
|
|
6
|
-
import pc from "picocolors";
|
|
7
5
|
import { existsSync, mkdirSync } from "fs";
|
|
8
6
|
import { resolve, basename } from "path";
|
|
9
7
|
import { detectProject, } from "./init/detect";
|
|
10
8
|
import { promptTargetDirectory, runFreshProjectWizard, runExistingProjectWizard, } from "./init/prompts";
|
|
11
9
|
import { scaffoldProject, installDependencies, printResults, } from "./init/scaffold";
|
|
10
|
+
import { ui } from "../ui";
|
|
12
11
|
export const initCommand = defineCommand({
|
|
13
12
|
meta: {
|
|
14
13
|
name: "init",
|
|
@@ -22,11 +21,12 @@ export const initCommand = defineCommand({
|
|
|
22
21
|
},
|
|
23
22
|
},
|
|
24
23
|
async run({ args }) {
|
|
24
|
+
ui.header();
|
|
25
25
|
const originalCwd = process.cwd();
|
|
26
26
|
// Step 1: Get target directory
|
|
27
27
|
const targetDir = await promptTargetDirectory(basename(originalCwd));
|
|
28
28
|
if (!targetDir) {
|
|
29
|
-
|
|
29
|
+
ui.info("Cancelled");
|
|
30
30
|
process.exit(0);
|
|
31
31
|
}
|
|
32
32
|
// Resolve the target directory
|
|
@@ -35,13 +35,13 @@ export const initCommand = defineCommand({
|
|
|
35
35
|
// Create directory if it doesn't exist
|
|
36
36
|
if (!existsSync(cwd)) {
|
|
37
37
|
mkdirSync(cwd, { recursive: true });
|
|
38
|
-
|
|
38
|
+
ui.success(`Created directory: ${targetDir}`);
|
|
39
39
|
}
|
|
40
40
|
// Detect project state in target directory
|
|
41
41
|
const info = await detectProject(cwd);
|
|
42
42
|
// Check for existing config (unless force)
|
|
43
43
|
if (info.hasElysianConfig && !args.force) {
|
|
44
|
-
|
|
44
|
+
ui.error("elysian.config.ts already exists. Use --force to overwrite.");
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
47
47
|
// Run appropriate wizard based on whether directory is empty
|
|
@@ -49,7 +49,7 @@ export const initCommand = defineCommand({
|
|
|
49
49
|
if (info.isEmpty) {
|
|
50
50
|
const result = await runFreshProjectWizard(name);
|
|
51
51
|
if (!result) {
|
|
52
|
-
|
|
52
|
+
ui.info("Cancelled");
|
|
53
53
|
process.exit(0);
|
|
54
54
|
}
|
|
55
55
|
answers = {
|
|
@@ -60,7 +60,7 @@ export const initCommand = defineCommand({
|
|
|
60
60
|
else {
|
|
61
61
|
const result = await runExistingProjectWizard(name, info.packageManager);
|
|
62
62
|
if (!result) {
|
|
63
|
-
|
|
63
|
+
ui.info("Cancelled");
|
|
64
64
|
process.exit(0);
|
|
65
65
|
}
|
|
66
66
|
answers = {
|
|
@@ -78,29 +78,29 @@ export const initCommand = defineCommand({
|
|
|
78
78
|
await installDependencies(cwd, answers.packageManager);
|
|
79
79
|
}
|
|
80
80
|
catch (error) {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
ui.error(error instanceof Error ? error.message : "Failed to install dependencies");
|
|
82
|
+
ui.info(`You can manually install with: ${answers.packageManager} add elysia @actuallyjamez/elysian`);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
// Print next steps
|
|
86
|
-
|
|
86
|
+
ui.blank();
|
|
87
87
|
const pm = answers.packageManager;
|
|
88
88
|
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
89
|
+
ui.success("Project initialized!");
|
|
90
|
+
ui.blank();
|
|
91
|
+
ui.section("Next steps");
|
|
89
92
|
// If we created in a subdirectory, tell user to cd into it
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(` ${pc.bold("Next steps")}:`);
|
|
94
|
-
console.log();
|
|
95
|
-
if (cdStep) {
|
|
96
|
-
console.log(` ${cdStep}`);
|
|
93
|
+
if (targetDir !== ".") {
|
|
94
|
+
console.log(` cd ${targetDir}`);
|
|
95
|
+
ui.blank();
|
|
97
96
|
}
|
|
98
97
|
if (!answers.installDeps) {
|
|
99
98
|
console.log(` ${pm} add elysia @actuallyjamez/elysian`);
|
|
100
|
-
|
|
99
|
+
ui.blank();
|
|
101
100
|
}
|
|
102
101
|
console.log(` ${runCmd} elysian build`);
|
|
103
|
-
|
|
102
|
+
ui.blank();
|
|
104
103
|
console.log(` cd terraform && terraform init && terraform apply`);
|
|
104
|
+
ui.blank();
|
|
105
105
|
},
|
|
106
106
|
});
|
package/dist/cli/ui.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI UI utilities for consistent styling across commands
|
|
3
|
+
*/
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
/**
|
|
6
|
+
* Format duration in human-readable format
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatDuration(ms: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Format bytes in human-readable format
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatSize(bytes: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Get color function for HTTP method
|
|
15
|
+
*/
|
|
16
|
+
export declare function methodColor(method: string): (text: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Spinner class for showing progress on long-running operations
|
|
19
|
+
*/
|
|
20
|
+
export declare class Spinner {
|
|
21
|
+
private message;
|
|
22
|
+
private frameIndex;
|
|
23
|
+
private interval;
|
|
24
|
+
private startTime;
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
start(): this;
|
|
27
|
+
stop(): number;
|
|
28
|
+
succeed(message?: string): number;
|
|
29
|
+
fail(message?: string): number;
|
|
30
|
+
update(message: string): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a new spinner
|
|
34
|
+
*/
|
|
35
|
+
export declare function createSpinner(message: string): Spinner;
|
|
36
|
+
/**
|
|
37
|
+
* UI output functions with consistent styling
|
|
38
|
+
*/
|
|
39
|
+
export declare const ui: {
|
|
40
|
+
/**
|
|
41
|
+
* Clear the terminal screen
|
|
42
|
+
*/
|
|
43
|
+
clear: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Print header with version and optional mode
|
|
46
|
+
*/
|
|
47
|
+
header: (mode?: string) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Success message with green checkmark
|
|
50
|
+
*/
|
|
51
|
+
success: (msg: string) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Error message with red X
|
|
54
|
+
*/
|
|
55
|
+
error: (msg: string) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Warning message with yellow exclamation
|
|
58
|
+
*/
|
|
59
|
+
warn: (msg: string) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Info message with dim arrow
|
|
62
|
+
*/
|
|
63
|
+
info: (msg: string) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Print a section header
|
|
66
|
+
*/
|
|
67
|
+
section: (title: string) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Print a horizontal divider
|
|
70
|
+
*/
|
|
71
|
+
divider: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Print a blank line
|
|
74
|
+
*/
|
|
75
|
+
blank: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Print indented text
|
|
78
|
+
*/
|
|
79
|
+
indent: (msg: string, level?: number) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Print a key-value pair
|
|
82
|
+
*/
|
|
83
|
+
keyValue: (key: string, value: string, indent?: number) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Print a labeled item (like lambda name with size)
|
|
86
|
+
*/
|
|
87
|
+
labeled: (label: string, suffix?: string) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Print a route line with method coloring
|
|
90
|
+
*/
|
|
91
|
+
route: (method: string, path: string, params?: string[], maxPathLen?: number) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Print watch mode status box
|
|
94
|
+
*/
|
|
95
|
+
watchBox: (options: {
|
|
96
|
+
watching: string[];
|
|
97
|
+
output: string;
|
|
98
|
+
localstack?: boolean;
|
|
99
|
+
}) => void;
|
|
100
|
+
/**
|
|
101
|
+
* Print terraform outputs
|
|
102
|
+
*/
|
|
103
|
+
outputs: (outputs: Record<string, unknown>) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Print build/deploy summary with timing
|
|
106
|
+
*/
|
|
107
|
+
summary: (options: {
|
|
108
|
+
lambdas: number;
|
|
109
|
+
routes?: number;
|
|
110
|
+
duration: number;
|
|
111
|
+
action?: string;
|
|
112
|
+
}) => void;
|
|
113
|
+
};
|
|
114
|
+
export { pc };
|
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI UI utilities for consistent styling across commands
|
|
3
|
+
*/
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { version } from "../core/version";
|
|
6
|
+
/**
|
|
7
|
+
* Format duration in human-readable format
|
|
8
|
+
*/
|
|
9
|
+
export function formatDuration(ms) {
|
|
10
|
+
if (ms < 1000)
|
|
11
|
+
return `${ms}ms`;
|
|
12
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Format bytes in human-readable format
|
|
16
|
+
*/
|
|
17
|
+
export function formatSize(bytes) {
|
|
18
|
+
if (bytes < 1024)
|
|
19
|
+
return `${bytes} B`;
|
|
20
|
+
if (bytes < 1024 * 1024)
|
|
21
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
22
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get color function for HTTP method
|
|
26
|
+
*/
|
|
27
|
+
export function methodColor(method) {
|
|
28
|
+
switch (method.toUpperCase()) {
|
|
29
|
+
case "GET":
|
|
30
|
+
return pc.green;
|
|
31
|
+
case "POST":
|
|
32
|
+
return pc.blue;
|
|
33
|
+
case "PUT":
|
|
34
|
+
return pc.yellow;
|
|
35
|
+
case "DELETE":
|
|
36
|
+
return pc.red;
|
|
37
|
+
case "PATCH":
|
|
38
|
+
return pc.magenta;
|
|
39
|
+
default:
|
|
40
|
+
return pc.white;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Spinner frames for loading animation
|
|
45
|
+
*/
|
|
46
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
47
|
+
/**
|
|
48
|
+
* Spinner class for showing progress on long-running operations
|
|
49
|
+
*/
|
|
50
|
+
export class Spinner {
|
|
51
|
+
message;
|
|
52
|
+
frameIndex = 0;
|
|
53
|
+
interval = null;
|
|
54
|
+
startTime = 0;
|
|
55
|
+
constructor(message) {
|
|
56
|
+
this.message = message;
|
|
57
|
+
}
|
|
58
|
+
start() {
|
|
59
|
+
this.startTime = Date.now();
|
|
60
|
+
this.frameIndex = 0;
|
|
61
|
+
// Write initial frame
|
|
62
|
+
process.stdout.write(` ${pc.cyan(SPINNER_FRAMES[0])} ${this.message}`);
|
|
63
|
+
this.interval = setInterval(() => {
|
|
64
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
65
|
+
// Move cursor to start of line and rewrite
|
|
66
|
+
process.stdout.write(`\r ${pc.cyan(SPINNER_FRAMES[this.frameIndex])} ${this.message}`);
|
|
67
|
+
}, 80);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
if (this.interval) {
|
|
72
|
+
clearInterval(this.interval);
|
|
73
|
+
this.interval = null;
|
|
74
|
+
}
|
|
75
|
+
// Clear the spinner line
|
|
76
|
+
process.stdout.write("\r" + " ".repeat(this.message.length + 10) + "\r");
|
|
77
|
+
return Date.now() - this.startTime;
|
|
78
|
+
}
|
|
79
|
+
succeed(message) {
|
|
80
|
+
const duration = this.stop();
|
|
81
|
+
console.log(` ${pc.green("✓")} ${message || this.message}`);
|
|
82
|
+
return duration;
|
|
83
|
+
}
|
|
84
|
+
fail(message) {
|
|
85
|
+
const duration = this.stop();
|
|
86
|
+
console.log(` ${pc.red("✗")} ${message || this.message}`);
|
|
87
|
+
return duration;
|
|
88
|
+
}
|
|
89
|
+
update(message) {
|
|
90
|
+
this.message = message;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a new spinner
|
|
95
|
+
*/
|
|
96
|
+
export function createSpinner(message) {
|
|
97
|
+
return new Spinner(message);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* UI output functions with consistent styling
|
|
101
|
+
*/
|
|
102
|
+
export const ui = {
|
|
103
|
+
/**
|
|
104
|
+
* Clear the terminal screen
|
|
105
|
+
*/
|
|
106
|
+
clear: () => {
|
|
107
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* Print header with version and optional mode
|
|
111
|
+
*/
|
|
112
|
+
header: (mode) => {
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(` ${pc.bold(pc.cyan("elysian"))} ${pc.dim(`v${version}`)}${mode ? ` ${mode}` : ""}`);
|
|
115
|
+
console.log();
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Success message with green checkmark
|
|
119
|
+
*/
|
|
120
|
+
success: (msg) => {
|
|
121
|
+
console.log(` ${pc.green("✓")} ${msg}`);
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Error message with red X
|
|
125
|
+
*/
|
|
126
|
+
error: (msg) => {
|
|
127
|
+
console.log(` ${pc.red("✗")} ${msg}`);
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* Warning message with yellow exclamation
|
|
131
|
+
*/
|
|
132
|
+
warn: (msg) => {
|
|
133
|
+
console.log(` ${pc.yellow("!")} ${msg}`);
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Info message with dim arrow
|
|
137
|
+
*/
|
|
138
|
+
info: (msg) => {
|
|
139
|
+
console.log(` ${pc.dim("›")} ${msg}`);
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Print a section header
|
|
143
|
+
*/
|
|
144
|
+
section: (title) => {
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(` ${pc.bold(title)}`);
|
|
147
|
+
console.log();
|
|
148
|
+
},
|
|
149
|
+
/**
|
|
150
|
+
* Print a horizontal divider
|
|
151
|
+
*/
|
|
152
|
+
divider: () => {
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(pc.dim(" " + "─".repeat(40)));
|
|
155
|
+
console.log();
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Print a blank line
|
|
159
|
+
*/
|
|
160
|
+
blank: () => {
|
|
161
|
+
console.log();
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* Print indented text
|
|
165
|
+
*/
|
|
166
|
+
indent: (msg, level = 1) => {
|
|
167
|
+
console.log(" ".repeat(level) + msg);
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* Print a key-value pair
|
|
171
|
+
*/
|
|
172
|
+
keyValue: (key, value, indent = 1) => {
|
|
173
|
+
console.log(" ".repeat(indent) + `${pc.dim(key + ":")} ${value}`);
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* Print a labeled item (like lambda name with size)
|
|
177
|
+
*/
|
|
178
|
+
labeled: (label, suffix) => {
|
|
179
|
+
const suffixStr = suffix ? pc.dim(` (${suffix})`) : "";
|
|
180
|
+
console.log(` ${pc.dim("λ")} ${pc.bold(label)}${suffixStr}`);
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* Print a route line with method coloring
|
|
184
|
+
*/
|
|
185
|
+
route: (method, path, params, maxPathLen) => {
|
|
186
|
+
const colorFn = methodColor(method);
|
|
187
|
+
const methodStr = colorFn(method.padEnd(6));
|
|
188
|
+
const pathStr = maxPathLen ? path.padEnd(maxPathLen + 2) : path;
|
|
189
|
+
const paramsStr = params && params.length > 0 ? pc.dim(` [${params.join(", ")}]`) : "";
|
|
190
|
+
console.log(` ${methodStr} ${pathStr}${paramsStr}`);
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Print watch mode status box
|
|
194
|
+
*/
|
|
195
|
+
watchBox: (options) => {
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(pc.dim(" " + "─".repeat(40)));
|
|
198
|
+
console.log();
|
|
199
|
+
for (const dir of options.watching) {
|
|
200
|
+
console.log(` ${pc.dim("Watching")} ${dir}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(` ${pc.dim("Output")} ${options.output}`);
|
|
203
|
+
if (options.localstack) {
|
|
204
|
+
console.log(` ${pc.dim("Deploy")} ${pc.green("LocalStack")} ${pc.dim("(tflocal)")}`);
|
|
205
|
+
}
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(` ${pc.dim("Press Ctrl+C to stop")}`);
|
|
208
|
+
console.log();
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* Print terraform outputs
|
|
212
|
+
*/
|
|
213
|
+
outputs: (outputs) => {
|
|
214
|
+
if (Object.keys(outputs).length === 0)
|
|
215
|
+
return;
|
|
216
|
+
console.log();
|
|
217
|
+
console.log(` ${pc.bold("Outputs")}`);
|
|
218
|
+
console.log();
|
|
219
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
220
|
+
const valueStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
221
|
+
console.log(` ${pc.dim(key + ":")} ${pc.cyan(valueStr)}`);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Print build/deploy summary with timing
|
|
226
|
+
*/
|
|
227
|
+
summary: (options) => {
|
|
228
|
+
const action = options.action || "Built";
|
|
229
|
+
const routeStr = options.routes !== undefined ? ` (${options.routes} routes)` : "";
|
|
230
|
+
console.log(` ${pc.green("✓")} ${action} ${pc.bold(String(options.lambdas))} lambda${options.lambdas === 1 ? "" : "s"}${routeStr} in ${pc.bold(formatDuration(options.duration))}`);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
export { pc };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStack detection and tflocal execution utilities
|
|
3
|
+
*/
|
|
4
|
+
export interface TerraformOutput {
|
|
5
|
+
[key: string]: {
|
|
6
|
+
value: unknown;
|
|
7
|
+
type?: string;
|
|
8
|
+
sensitive?: boolean;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface TfResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if LocalStack is running by hitting the health endpoint
|
|
17
|
+
*/
|
|
18
|
+
export declare function isLocalStackRunning(endpoint?: string): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Check if tflocal CLI is installed
|
|
21
|
+
*/
|
|
22
|
+
export declare function isTfLocalInstalled(): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if terraform has been initialized in the given directory
|
|
25
|
+
*/
|
|
26
|
+
export declare function isTerraformInitialized(terraformDir: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Run tflocal init
|
|
29
|
+
*/
|
|
30
|
+
export declare function runTfLocalInit(terraformDir: string): Promise<TfResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Run tflocal apply with auto-approve
|
|
33
|
+
*/
|
|
34
|
+
export declare function runTfLocalApply(terraformDir: string): Promise<TfResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Transform AWS URLs to LocalStack URLs
|
|
37
|
+
* Converts URLs like:
|
|
38
|
+
* https://abc123.execute-api.eu-west-2.amazonaws.com/
|
|
39
|
+
* To:
|
|
40
|
+
* http://abc123.execute-api.localhost.localstack.cloud:4566/
|
|
41
|
+
*/
|
|
42
|
+
export declare function transformToLocalStackUrl(value: unknown): unknown;
|
|
43
|
+
/**
|
|
44
|
+
* Transform all URLs in terraform outputs to LocalStack format
|
|
45
|
+
*/
|
|
46
|
+
export declare function transformOutputsForLocalStack(outputs: Record<string, unknown>): Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Get terraform outputs as JSON
|
|
49
|
+
*/
|
|
50
|
+
export declare function getTerraformOutputs(terraformDir: string, transformForLocalStack?: boolean): Promise<Record<string, unknown> | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Detect LocalStack availability
|
|
53
|
+
* Returns an object with detection results and reasons
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectLocalStack(): Promise<{
|
|
56
|
+
available: boolean;
|
|
57
|
+
localstackRunning: boolean;
|
|
58
|
+
tfLocalInstalled: boolean;
|
|
59
|
+
}>;
|