@actuallyjamez/elysian 0.5.0 → 0.6.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 +28 -69
- package/dist/cli/commands/dev.d.ts +6 -1
- package/dist/cli/commands/dev.js +282 -38
- package/dist/cli/commands/generate-iac.js +41 -23
- package/dist/cli/commands/init/scaffold.js +6 -6
- 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 +1 -1
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Build command - Production build of all lambdas
|
|
3
3
|
*/
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import { readdirSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { readdirSync, mkdirSync, existsSync, rmSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import pc from "picocolors";
|
|
8
7
|
import { loadConfig } from "../../core/config";
|
|
9
8
|
import { bundleLambda } from "../../core/bundler";
|
|
10
9
|
import { packageLambda } from "../../core/packager";
|
|
@@ -13,19 +12,7 @@ import { writeTerraformVars } from "../../core/terraform";
|
|
|
13
12
|
import { shouldGenerateOpenApi, writeOpenApiLambda, cleanupOpenApiLambda, } from "../../core/openapi";
|
|
14
13
|
import { createWrapperEntry } from "../../core/handler-wrapper";
|
|
15
14
|
import { getLambdaBundleName } from "../../core/naming";
|
|
16
|
-
import {
|
|
17
|
-
function formatDuration(ms) {
|
|
18
|
-
if (ms < 1000)
|
|
19
|
-
return `${ms}ms`;
|
|
20
|
-
return `${(ms / 1000).toFixed(2)}s`;
|
|
21
|
-
}
|
|
22
|
-
function formatSize(bytes) {
|
|
23
|
-
if (bytes < 1024)
|
|
24
|
-
return `${bytes} B`;
|
|
25
|
-
if (bytes < 1024 * 1024)
|
|
26
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
27
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
28
|
-
}
|
|
15
|
+
import { ui, pc, formatDuration, formatSize } from "../ui";
|
|
29
16
|
export const buildCommand = defineCommand({
|
|
30
17
|
meta: {
|
|
31
18
|
name: "build",
|
|
@@ -45,16 +32,14 @@ export const buildCommand = defineCommand({
|
|
|
45
32
|
process.env.NODE_ENV = "production";
|
|
46
33
|
}
|
|
47
34
|
// Header
|
|
48
|
-
|
|
49
|
-
console.log(` ${pc.bold(pc.cyan("elysian"))} ${pc.dim(`v${version}`)} ${args.prod ? pc.yellow("production") : pc.dim("development")}`);
|
|
50
|
-
console.log();
|
|
35
|
+
ui.header(args.prod ? pc.yellow("production") : "development");
|
|
51
36
|
// Load config
|
|
52
37
|
let config;
|
|
53
38
|
try {
|
|
54
39
|
config = await loadConfig();
|
|
55
40
|
}
|
|
56
41
|
catch (error) {
|
|
57
|
-
|
|
42
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
58
43
|
process.exit(1);
|
|
59
44
|
}
|
|
60
45
|
const name = config.name;
|
|
@@ -63,7 +48,7 @@ export const buildCommand = defineCommand({
|
|
|
63
48
|
const terraformDir = join(process.cwd(), config.terraform.outputDir);
|
|
64
49
|
// Ensure directories exist
|
|
65
50
|
if (!existsSync(lambdasDir)) {
|
|
66
|
-
|
|
51
|
+
ui.error(`Lambdas directory not found: ${lambdasDir}`);
|
|
67
52
|
process.exit(1);
|
|
68
53
|
}
|
|
69
54
|
mkdirSync(outputDir, { recursive: true });
|
|
@@ -71,7 +56,7 @@ export const buildCommand = defineCommand({
|
|
|
71
56
|
// Get lambda files
|
|
72
57
|
let lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
|
|
73
58
|
if (lambdaFiles.length === 0) {
|
|
74
|
-
|
|
59
|
+
ui.warn(`No lambda files found in ${config.lambdasDir}`);
|
|
75
60
|
return;
|
|
76
61
|
}
|
|
77
62
|
// Generate OpenAPI aggregator if enabled
|
|
@@ -80,54 +65,53 @@ export const buildCommand = defineCommand({
|
|
|
80
65
|
lambdaFiles.push("__openapi__.ts");
|
|
81
66
|
}
|
|
82
67
|
// Build phase
|
|
83
|
-
|
|
68
|
+
ui.success(`Compiling ${lambdaFiles.length} lambdas...`);
|
|
84
69
|
const tempDir = join(outputDir, "__temp__");
|
|
85
70
|
mkdirSync(tempDir, { recursive: true });
|
|
86
71
|
const buildResults = [];
|
|
87
72
|
for (const file of lambdaFiles) {
|
|
88
|
-
const
|
|
89
|
-
const bundleName = getLambdaBundleName(name,
|
|
73
|
+
const lambdaName = file.replace(/\.ts$/, "");
|
|
74
|
+
const bundleName = getLambdaBundleName(name, lambdaName);
|
|
90
75
|
const inputPath = join(lambdasDir, file);
|
|
91
76
|
// Create wrapper entry that imports the original and exports handler
|
|
92
|
-
const wrapperPath = join(tempDir, `${
|
|
77
|
+
const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
|
|
93
78
|
const wrapperContent = createWrapperEntry(inputPath);
|
|
94
79
|
await Bun.write(wrapperPath, wrapperContent);
|
|
95
80
|
// Bundle the wrapper with prefixed name
|
|
96
81
|
const result = await bundleLambda(bundleName, wrapperPath, outputDir, config);
|
|
97
|
-
buildResults.push({ ...result, name, bundleName });
|
|
82
|
+
buildResults.push({ ...result, name: lambdaName, bundleName });
|
|
98
83
|
if (!result.success) {
|
|
99
|
-
|
|
84
|
+
ui.error(`Failed to build ${lambdaName}: ${result.error}`);
|
|
100
85
|
process.exit(1);
|
|
101
86
|
}
|
|
102
87
|
}
|
|
103
88
|
// Clean up temp directory
|
|
104
|
-
const { rmSync } = await import("fs");
|
|
105
89
|
rmSync(tempDir, { recursive: true, force: true });
|
|
106
90
|
// Clean up generated OpenAPI file
|
|
107
91
|
if (shouldGenerateOpenApi(config)) {
|
|
108
92
|
await cleanupOpenApiLambda(lambdasDir);
|
|
109
93
|
}
|
|
110
94
|
// Package phase
|
|
111
|
-
|
|
95
|
+
ui.success("Packaging lambdas...");
|
|
112
96
|
const packageSizes = new Map();
|
|
113
97
|
for (const file of lambdaFiles) {
|
|
114
|
-
const
|
|
115
|
-
const bundleName = getLambdaBundleName(name,
|
|
98
|
+
const lambdaName = file.replace(/\.ts$/, "");
|
|
99
|
+
const bundleName = getLambdaBundleName(name, lambdaName);
|
|
116
100
|
const jsPath = join(outputDir, `${bundleName}.js`);
|
|
117
101
|
const result = await packageLambda(bundleName, jsPath, outputDir);
|
|
118
102
|
if (!result.success) {
|
|
119
|
-
|
|
103
|
+
ui.error(`Failed to package ${lambdaName}: ${result.error}`);
|
|
120
104
|
process.exit(1);
|
|
121
105
|
}
|
|
122
106
|
// Get zip size (store by original name for display)
|
|
123
107
|
const zipPath = join(outputDir, `${bundleName}.zip`);
|
|
124
108
|
const stat = await Bun.file(zipPath).stat();
|
|
125
109
|
if (stat) {
|
|
126
|
-
packageSizes.set(
|
|
110
|
+
packageSizes.set(lambdaName, stat.size);
|
|
127
111
|
}
|
|
128
112
|
}
|
|
129
113
|
// Generate manifest
|
|
130
|
-
|
|
114
|
+
ui.success("Generating manifest...");
|
|
131
115
|
try {
|
|
132
116
|
const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, name);
|
|
133
117
|
// Write JSON manifest
|
|
@@ -137,10 +121,8 @@ export const buildCommand = defineCommand({
|
|
|
137
121
|
await writeTerraformVars(manifest, config);
|
|
138
122
|
// Duration
|
|
139
123
|
const duration = Date.now() - startTime;
|
|
140
|
-
// Route table
|
|
141
|
-
|
|
142
|
-
console.log(` ${pc.bold("Routes")}`);
|
|
143
|
-
console.log();
|
|
124
|
+
// Route table
|
|
125
|
+
ui.section("Routes");
|
|
144
126
|
// Group routes by lambda (use original name for display)
|
|
145
127
|
const routesByLambda = new Map();
|
|
146
128
|
for (const route of manifest.routes) {
|
|
@@ -152,47 +134,24 @@ export const buildCommand = defineCommand({
|
|
|
152
134
|
existing.push(route);
|
|
153
135
|
routesByLambda.set(displayName, existing);
|
|
154
136
|
}
|
|
155
|
-
// Method colors
|
|
156
|
-
const methodColor = (method) => {
|
|
157
|
-
switch (method) {
|
|
158
|
-
case "GET":
|
|
159
|
-
return pc.green;
|
|
160
|
-
case "POST":
|
|
161
|
-
return pc.blue;
|
|
162
|
-
case "PUT":
|
|
163
|
-
return pc.yellow;
|
|
164
|
-
case "DELETE":
|
|
165
|
-
return pc.red;
|
|
166
|
-
case "PATCH":
|
|
167
|
-
return pc.magenta;
|
|
168
|
-
default:
|
|
169
|
-
return pc.white;
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
137
|
// Find longest path for alignment
|
|
173
138
|
const maxPathLen = Math.max(...manifest.routes.map((r) => r.path.length));
|
|
174
139
|
for (const [displayName, routes] of routesByLambda) {
|
|
175
140
|
const size = packageSizes.get(displayName);
|
|
176
|
-
const sizeStr = size ?
|
|
177
|
-
|
|
141
|
+
const sizeStr = size ? formatSize(size) : undefined;
|
|
142
|
+
ui.labeled(displayName, sizeStr);
|
|
178
143
|
for (const route of routes) {
|
|
179
|
-
|
|
180
|
-
const path = route.path.padEnd(maxPathLen + 2);
|
|
181
|
-
const params = route.pathParameters.length > 0
|
|
182
|
-
? pc.dim(` [${route.pathParameters.join(", ")}]`)
|
|
183
|
-
: "";
|
|
184
|
-
console.log(` ${method} ${path}${params}`);
|
|
144
|
+
ui.route(route.method, route.path, route.pathParameters, maxPathLen);
|
|
185
145
|
}
|
|
186
|
-
|
|
146
|
+
ui.blank();
|
|
187
147
|
}
|
|
188
148
|
// Summary footer
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log();
|
|
149
|
+
ui.divider();
|
|
150
|
+
ui.success(`Compiled ${pc.bold(String(manifest.lambdas.length))} lambdas (${manifest.routes.length} routes) in ${pc.bold(formatDuration(duration))}`);
|
|
151
|
+
ui.blank();
|
|
193
152
|
}
|
|
194
153
|
catch (error) {
|
|
195
|
-
|
|
154
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
196
155
|
process.exit(1);
|
|
197
156
|
}
|
|
198
157
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev command - Watch mode
|
|
2
|
+
* Dev command - Watch mode with LocalStack integration
|
|
3
3
|
*/
|
|
4
4
|
export declare const devCommand: import("citty").CommandDef<{
|
|
5
5
|
"no-package": {
|
|
@@ -7,4 +7,9 @@ export declare const devCommand: import("citty").CommandDef<{
|
|
|
7
7
|
description: string;
|
|
8
8
|
default: false;
|
|
9
9
|
};
|
|
10
|
+
"no-localstack": {
|
|
11
|
+
type: "boolean";
|
|
12
|
+
description: string;
|
|
13
|
+
default: false;
|
|
14
|
+
};
|
|
10
15
|
}>;
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev command - Watch mode
|
|
2
|
+
* Dev command - Watch mode with LocalStack integration
|
|
3
3
|
*/
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import
|
|
6
|
-
import { watch, readdirSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { watch, readdirSync, mkdirSync, existsSync, rmSync } from "fs";
|
|
7
6
|
import { join } from "path";
|
|
8
7
|
import { loadConfig } from "../../core/config";
|
|
9
8
|
import { bundleLambda } from "../../core/bundler";
|
|
10
9
|
import { packageLambda } from "../../core/packager";
|
|
11
10
|
import { createWrapperEntry } from "../../core/handler-wrapper";
|
|
12
11
|
import { getLambdaBundleName } from "../../core/naming";
|
|
12
|
+
import { generateManifest, writeManifest } from "../../core/manifest";
|
|
13
|
+
import { writeTerraformVars } from "../../core/terraform";
|
|
14
|
+
import { shouldGenerateOpenApi, writeOpenApiLambda, cleanupOpenApiLambda, } from "../../core/openapi";
|
|
15
|
+
import { detectLocalStack, isTerraformInitialized, runTfLocalInit, runTfLocalApply, getTerraformOutputs, } from "../../core/localstack";
|
|
16
|
+
import { ui, pc, createSpinner, formatDuration } from "../ui";
|
|
13
17
|
export const devCommand = defineCommand({
|
|
14
18
|
meta: {
|
|
15
19
|
name: "dev",
|
|
16
|
-
description: "Watch mode - rebuild lambdas on file changes",
|
|
20
|
+
description: "Watch mode - rebuild lambdas on file changes with LocalStack deploy",
|
|
17
21
|
},
|
|
18
22
|
args: {
|
|
19
23
|
"no-package": {
|
|
@@ -21,91 +25,331 @@ export const devCommand = defineCommand({
|
|
|
21
25
|
description: "Skip creating zip files (faster rebuilds)",
|
|
22
26
|
default: false,
|
|
23
27
|
},
|
|
28
|
+
"no-localstack": {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Disable LocalStack integration",
|
|
31
|
+
default: false,
|
|
32
|
+
},
|
|
24
33
|
},
|
|
25
34
|
async run({ args }) {
|
|
26
|
-
|
|
35
|
+
// Initial setup
|
|
36
|
+
ui.header(pc.dim("dev"));
|
|
37
|
+
// Load config
|
|
27
38
|
let config;
|
|
28
39
|
try {
|
|
29
40
|
config = await loadConfig();
|
|
41
|
+
ui.success("Loaded configuration");
|
|
30
42
|
}
|
|
31
43
|
catch (error) {
|
|
32
|
-
|
|
44
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
33
45
|
process.exit(1);
|
|
34
46
|
}
|
|
35
47
|
const name = config.name;
|
|
36
48
|
const lambdasDir = join(process.cwd(), config.lambdasDir);
|
|
37
49
|
const outputDir = join(process.cwd(), config.outputDir);
|
|
50
|
+
const terraformDir = join(process.cwd(), config.terraform.outputDir);
|
|
38
51
|
const tempDir = join(outputDir, "__temp__");
|
|
39
52
|
// Ensure directories exist
|
|
40
53
|
if (!existsSync(lambdasDir)) {
|
|
41
|
-
|
|
54
|
+
ui.error(`Lambdas directory not found: ${lambdasDir}`);
|
|
42
55
|
process.exit(1);
|
|
43
56
|
}
|
|
44
57
|
mkdirSync(outputDir, { recursive: true });
|
|
45
58
|
mkdirSync(tempDir, { recursive: true });
|
|
59
|
+
// Detect LocalStack
|
|
60
|
+
let localstackEnabled = false;
|
|
61
|
+
if (!args["no-localstack"]) {
|
|
62
|
+
const detection = await detectLocalStack();
|
|
63
|
+
if (detection.available) {
|
|
64
|
+
localstackEnabled = true;
|
|
65
|
+
ui.success("LocalStack detected");
|
|
66
|
+
// Check if terraform is initialized
|
|
67
|
+
if (!isTerraformInitialized(terraformDir)) {
|
|
68
|
+
const spinner = createSpinner("Initializing terraform...").start();
|
|
69
|
+
const initResult = await runTfLocalInit(terraformDir);
|
|
70
|
+
if (!initResult.success) {
|
|
71
|
+
spinner.fail("tflocal init failed");
|
|
72
|
+
console.log(pc.dim(initResult.error));
|
|
73
|
+
ui.warn("Continuing without LocalStack deploy");
|
|
74
|
+
localstackEnabled = false;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
spinner.succeed("Terraform initialized");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (!detection.localstackRunning) {
|
|
83
|
+
ui.warn("LocalStack not running - skipping deploy");
|
|
84
|
+
}
|
|
85
|
+
if (!detection.tfLocalInstalled) {
|
|
86
|
+
ui.warn("tflocal not installed - skipping deploy");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
46
90
|
// Get initial lambda files
|
|
47
|
-
|
|
91
|
+
let lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
|
|
48
92
|
if (lambdaFiles.length === 0) {
|
|
49
|
-
|
|
93
|
+
ui.warn(`No lambda files found in ${config.lambdasDir}`);
|
|
50
94
|
}
|
|
95
|
+
// Track last terraform outputs and build info for display
|
|
96
|
+
let lastOutputs = null;
|
|
97
|
+
let lastBuildInfo = null;
|
|
51
98
|
// Build function for a single lambda
|
|
52
99
|
async function buildSingleLambda(filename) {
|
|
53
|
-
const
|
|
54
|
-
const bundleName = getLambdaBundleName(name,
|
|
100
|
+
const lambdaName = filename.replace(/\.ts$/, "");
|
|
101
|
+
const bundleName = getLambdaBundleName(name, lambdaName);
|
|
55
102
|
const inputPath = join(lambdasDir, filename);
|
|
56
103
|
// Create wrapper entry
|
|
57
|
-
const wrapperPath = join(tempDir, `${
|
|
104
|
+
const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
|
|
58
105
|
const wrapperContent = createWrapperEntry(inputPath);
|
|
59
106
|
await Bun.write(wrapperPath, wrapperContent);
|
|
60
107
|
// Bundle with prefixed name
|
|
61
108
|
const buildResult = await bundleLambda(bundleName, wrapperPath, outputDir, config);
|
|
62
109
|
if (!buildResult.success) {
|
|
63
|
-
consola.error(`Failed to build ${name}: ${buildResult.error}`);
|
|
64
110
|
return false;
|
|
65
111
|
}
|
|
66
|
-
consola.success(`Built ${bundleName}.js`);
|
|
67
112
|
// Package if not disabled
|
|
68
113
|
if (!args["no-package"]) {
|
|
69
114
|
const jsPath = join(outputDir, `${bundleName}.js`);
|
|
70
115
|
const packageResult = await packageLambda(bundleName, jsPath, outputDir);
|
|
71
116
|
if (!packageResult.success) {
|
|
72
|
-
consola.error(`Failed to package ${name}: ${packageResult.error}`);
|
|
73
117
|
return false;
|
|
74
118
|
}
|
|
75
|
-
consola.success(`Packaged ${bundleName}.zip`);
|
|
76
119
|
}
|
|
77
120
|
return true;
|
|
78
121
|
}
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
122
|
+
// Build all lambdas (including OpenAPI if enabled)
|
|
123
|
+
async function buildAll() {
|
|
124
|
+
// Refresh lambda file list
|
|
125
|
+
lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
|
|
126
|
+
const filesToBuild = [...lambdaFiles];
|
|
127
|
+
// Generate OpenAPI aggregator if enabled
|
|
128
|
+
if (shouldGenerateOpenApi(config)) {
|
|
129
|
+
await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
|
|
130
|
+
filesToBuild.push("__openapi__.ts");
|
|
131
|
+
}
|
|
132
|
+
// Build all lambdas
|
|
133
|
+
for (const file of filesToBuild) {
|
|
134
|
+
const success = await buildSingleLambda(file);
|
|
135
|
+
if (!success) {
|
|
136
|
+
return { success: false, count: 0 };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Cleanup OpenAPI source file
|
|
140
|
+
if (shouldGenerateOpenApi(config)) {
|
|
141
|
+
await cleanupOpenApiLambda(lambdasDir);
|
|
142
|
+
}
|
|
143
|
+
return { success: true, count: filesToBuild.length };
|
|
144
|
+
}
|
|
145
|
+
// Generate manifest and terraform vars
|
|
146
|
+
async function generateManifestFiles() {
|
|
147
|
+
try {
|
|
148
|
+
const filesToManifest = [...lambdaFiles];
|
|
149
|
+
if (shouldGenerateOpenApi(config)) {
|
|
150
|
+
filesToManifest.push("__openapi__.ts");
|
|
151
|
+
}
|
|
152
|
+
const manifest = await generateManifest(filesToManifest, outputDir, config.openapi.enabled, name);
|
|
153
|
+
const manifestPath = join(outputDir, "manifest.json");
|
|
154
|
+
await writeManifest(manifest, manifestPath);
|
|
155
|
+
await writeTerraformVars(manifest, config);
|
|
156
|
+
return { success: true, routes: manifest.routes.length };
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return { success: false, routes: 0 };
|
|
160
|
+
}
|
|
83
161
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
// Deploy to LocalStack
|
|
163
|
+
async function deployToLocalStack() {
|
|
164
|
+
const applyResult = await runTfLocalApply(terraformDir);
|
|
165
|
+
if (!applyResult.success) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
// Get and store outputs (transformed to LocalStack URLs)
|
|
169
|
+
lastOutputs = await getTerraformOutputs(terraformDir, true);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
// Show the final status screen (Vite-like)
|
|
173
|
+
function showReadyScreen(trigger, failed) {
|
|
174
|
+
ui.clear();
|
|
175
|
+
ui.header(pc.dim("dev"));
|
|
176
|
+
if (failed) {
|
|
177
|
+
ui.error("Build failed");
|
|
178
|
+
if (trigger) {
|
|
179
|
+
ui.info(`Triggered by: ${trigger}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (lastBuildInfo) {
|
|
183
|
+
ui.success(`Ready in ${pc.bold(formatDuration(lastBuildInfo.duration))}`);
|
|
184
|
+
ui.blank();
|
|
185
|
+
// Show outputs prominently
|
|
186
|
+
if (lastOutputs && Object.keys(lastOutputs).length > 0) {
|
|
187
|
+
for (const [key, value] of Object.entries(lastOutputs)) {
|
|
188
|
+
const valueStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
189
|
+
console.log(` ${pc.dim("➜")} ${pc.bold(key)}: ${pc.cyan(valueStr)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
ui.blank();
|
|
193
|
+
console.log(pc.dim(` ${lastBuildInfo.lambdas} lambda${lastBuildInfo.lambdas === 1 ? "" : "s"} · ${lastBuildInfo.routes} routes`));
|
|
194
|
+
}
|
|
195
|
+
// Watch status
|
|
196
|
+
ui.blank();
|
|
197
|
+
console.log(pc.dim(" ─────────────────────────────────────"));
|
|
198
|
+
ui.blank();
|
|
199
|
+
const watchDirs = [config.lambdasDir];
|
|
200
|
+
if (localstackEnabled) {
|
|
201
|
+
watchDirs.push(config.terraform.outputDir);
|
|
202
|
+
}
|
|
203
|
+
console.log(` ${pc.dim("watching:")} ${watchDirs.join(", ")}`);
|
|
204
|
+
if (localstackEnabled) {
|
|
205
|
+
console.log(` ${pc.dim("deploy:")} ${pc.green("localstack")}`);
|
|
206
|
+
}
|
|
207
|
+
ui.blank();
|
|
208
|
+
console.log(pc.dim(" press ctrl+c to stop"));
|
|
209
|
+
ui.blank();
|
|
210
|
+
}
|
|
211
|
+
// Run the full build and deploy cycle
|
|
212
|
+
async function runBuildCycle(trigger) {
|
|
213
|
+
const cycleStart = Date.now();
|
|
214
|
+
// Show building status
|
|
215
|
+
ui.clear();
|
|
216
|
+
ui.header(pc.dim("dev"));
|
|
217
|
+
if (trigger) {
|
|
218
|
+
ui.info(`Change: ${trigger}`);
|
|
219
|
+
ui.blank();
|
|
220
|
+
}
|
|
221
|
+
// Build
|
|
222
|
+
const buildSpinner = createSpinner("Building...").start();
|
|
223
|
+
const buildResult = await buildAll();
|
|
224
|
+
if (!buildResult.success) {
|
|
225
|
+
buildSpinner.fail("Build failed");
|
|
226
|
+
showReadyScreen(trigger, true);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
buildSpinner.succeed(`Built ${buildResult.count} lambda${buildResult.count === 1 ? "" : "s"}`);
|
|
230
|
+
// Generate manifest
|
|
231
|
+
const manifestSpinner = createSpinner("Generating manifest...").start();
|
|
232
|
+
const manifestResult = await generateManifestFiles();
|
|
233
|
+
if (!manifestResult.success) {
|
|
234
|
+
manifestSpinner.fail("Manifest failed");
|
|
235
|
+
showReadyScreen(trigger, true);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
manifestSpinner.succeed("Generated manifest");
|
|
239
|
+
// Deploy if LocalStack enabled
|
|
240
|
+
if (localstackEnabled) {
|
|
241
|
+
const deploySpinner = createSpinner("Deploying...").start();
|
|
242
|
+
const deploySuccess = await deployToLocalStack();
|
|
243
|
+
if (!deploySuccess) {
|
|
244
|
+
deploySpinner.fail("Deploy failed");
|
|
245
|
+
showReadyScreen(trigger, true);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
deploySpinner.succeed("Deployed");
|
|
249
|
+
}
|
|
250
|
+
// Store build info
|
|
251
|
+
const duration = Date.now() - cycleStart;
|
|
252
|
+
lastBuildInfo = {
|
|
253
|
+
lambdas: buildResult.count,
|
|
254
|
+
routes: manifestResult.routes,
|
|
255
|
+
duration,
|
|
256
|
+
};
|
|
257
|
+
// Show ready screen
|
|
258
|
+
showReadyScreen(trigger);
|
|
259
|
+
}
|
|
260
|
+
// Run terraform-only deploy
|
|
261
|
+
async function runTerraformCycle(trigger) {
|
|
262
|
+
const cycleStart = Date.now();
|
|
263
|
+
ui.clear();
|
|
264
|
+
ui.header(pc.dim("dev"));
|
|
265
|
+
ui.info(`Terraform: ${trigger}`);
|
|
266
|
+
ui.blank();
|
|
267
|
+
const deploySpinner = createSpinner("Deploying...").start();
|
|
268
|
+
const deploySuccess = await deployToLocalStack();
|
|
269
|
+
if (!deploySuccess) {
|
|
270
|
+
deploySpinner.fail("Deploy failed");
|
|
271
|
+
showReadyScreen(trigger, true);
|
|
93
272
|
return;
|
|
94
273
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
consola.ready("Rebuild complete");
|
|
274
|
+
deploySpinner.succeed("Deployed");
|
|
275
|
+
// Update duration
|
|
276
|
+
if (lastBuildInfo) {
|
|
277
|
+
lastBuildInfo.duration = Date.now() - cycleStart;
|
|
100
278
|
}
|
|
279
|
+
showReadyScreen(trigger);
|
|
280
|
+
}
|
|
281
|
+
// Debounce timer for file changes
|
|
282
|
+
let debounceTimer = null;
|
|
283
|
+
const DEBOUNCE_MS = 150;
|
|
284
|
+
// Pending changes during debounce
|
|
285
|
+
let pendingTrigger = null;
|
|
286
|
+
let pendingIsTerraform = false;
|
|
287
|
+
// Handle file change with debouncing
|
|
288
|
+
function handleFileChange(trigger, isTerraform = false) {
|
|
289
|
+
pendingTrigger = trigger;
|
|
290
|
+
pendingIsTerraform = isTerraform;
|
|
291
|
+
if (debounceTimer) {
|
|
292
|
+
clearTimeout(debounceTimer);
|
|
293
|
+
}
|
|
294
|
+
debounceTimer = setTimeout(async () => {
|
|
295
|
+
debounceTimer = null;
|
|
296
|
+
const triggerName = pendingTrigger || trigger;
|
|
297
|
+
const terraformOnly = pendingIsTerraform;
|
|
298
|
+
pendingTrigger = null;
|
|
299
|
+
pendingIsTerraform = false;
|
|
300
|
+
if (terraformOnly && localstackEnabled) {
|
|
301
|
+
await runTerraformCycle(triggerName);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
await runBuildCycle(triggerName);
|
|
305
|
+
}
|
|
306
|
+
}, DEBOUNCE_MS);
|
|
307
|
+
}
|
|
308
|
+
// Initial build
|
|
309
|
+
await runBuildCycle();
|
|
310
|
+
// Set up lambda file watcher
|
|
311
|
+
const lambdaWatcher = watch(lambdasDir, { recursive: false }, (event, filename) => {
|
|
312
|
+
if (!filename ||
|
|
313
|
+
!filename.endsWith(".ts") ||
|
|
314
|
+
filename.startsWith("__")) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
handleFileChange(filename, false);
|
|
101
318
|
});
|
|
319
|
+
// Set up terraform watcher if LocalStack is enabled
|
|
320
|
+
let terraformWatcher = null;
|
|
321
|
+
if (localstackEnabled && existsSync(terraformDir)) {
|
|
322
|
+
terraformWatcher = watch(terraformDir, { recursive: false }, (event, filename) => {
|
|
323
|
+
if (!filename)
|
|
324
|
+
return;
|
|
325
|
+
// Skip auto-generated files
|
|
326
|
+
if (filename === config.terraform.tfvarsFilename ||
|
|
327
|
+
filename.endsWith(".auto.tfvars") ||
|
|
328
|
+
filename.startsWith(".terraform") ||
|
|
329
|
+
filename.endsWith(".tfstate") ||
|
|
330
|
+
filename.endsWith(".tfstate.backup")) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Only watch .tf files
|
|
334
|
+
if (!filename.endsWith(".tf")) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
handleFileChange(filename, true);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
102
340
|
// Handle graceful shutdown
|
|
103
341
|
process.on("SIGINT", () => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
342
|
+
ui.blank();
|
|
343
|
+
ui.info("Stopping...");
|
|
344
|
+
lambdaWatcher.close();
|
|
345
|
+
if (terraformWatcher) {
|
|
346
|
+
terraformWatcher.close();
|
|
347
|
+
}
|
|
348
|
+
// Clear debounce timer
|
|
349
|
+
if (debounceTimer) {
|
|
350
|
+
clearTimeout(debounceTimer);
|
|
351
|
+
}
|
|
107
352
|
// Clean up temp directory
|
|
108
|
-
const { rmSync } = require("fs");
|
|
109
353
|
try {
|
|
110
354
|
rmSync(tempDir, { recursive: true, force: true });
|
|
111
355
|
}
|