@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.
@@ -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 { version } from "../../core/version";
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
- console.log();
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
- console.log(` ${pc.red("✗")} ${error instanceof Error ? error.message : error}`);
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
- console.log(` ${pc.red("✗")} Lambdas directory not found: ${lambdasDir}`);
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
- console.log(` ${pc.yellow("!")} No lambda files found in ${config.lambdasDir}`);
59
+ ui.warn(`No lambda files found in ${config.lambdasDir}`);
75
60
  return;
76
61
  }
77
62
  // Generate OpenAPI aggregator if enabled
@@ -80,7 +65,7 @@ export const buildCommand = defineCommand({
80
65
  lambdaFiles.push("__openapi__.ts");
81
66
  }
82
67
  // Build phase
83
- console.log(` ${pc.green("✓")} Compiling ${lambdaFiles.length} lambdas...`);
68
+ ui.success(`Compiling ${lambdaFiles.length} lambdas...`);
84
69
  const tempDir = join(outputDir, "__temp__");
85
70
  mkdirSync(tempDir, { recursive: true });
86
71
  const buildResults = [];
@@ -96,19 +81,18 @@ export const buildCommand = defineCommand({
96
81
  const result = await bundleLambda(bundleName, wrapperPath, outputDir, config);
97
82
  buildResults.push({ ...result, name: lambdaName, bundleName });
98
83
  if (!result.success) {
99
- console.log(` ${pc.red("✗")} Failed to build ${lambdaName}: ${result.error}`);
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
- console.log(` ${pc.green("✓")} Packaging lambdas...`);
95
+ ui.success("Packaging lambdas...");
112
96
  const packageSizes = new Map();
113
97
  for (const file of lambdaFiles) {
114
98
  const lambdaName = file.replace(/\.ts$/, "");
@@ -116,7 +100,7 @@ export const buildCommand = defineCommand({
116
100
  const jsPath = join(outputDir, `${bundleName}.js`);
117
101
  const result = await packageLambda(bundleName, jsPath, outputDir);
118
102
  if (!result.success) {
119
- console.log(` ${pc.red("✗")} Failed to package ${lambdaName}: ${result.error}`);
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)
@@ -127,7 +111,7 @@ export const buildCommand = defineCommand({
127
111
  }
128
112
  }
129
113
  // Generate manifest
130
- console.log(` ${pc.green("✓")} Generating manifest...`);
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 header
141
- console.log();
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 ? pc.dim(` (${formatSize(size)})`) : "";
177
- console.log(` ${pc.dim("λ")} ${pc.bold(displayName)}${sizeStr}`);
141
+ const sizeStr = size ? formatSize(size) : undefined;
142
+ ui.labeled(displayName, sizeStr);
178
143
  for (const route of routes) {
179
- const method = methodColor(route.method)(route.method.padEnd(6));
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
- console.log();
146
+ ui.blank();
187
147
  }
188
148
  // Summary footer
189
- console.log(pc.dim(" " + "─".repeat(40)));
190
- console.log();
191
- console.log(` ${pc.green("✓")} Compiled ${pc.bold(String(manifest.lambdas.length))} lambdas (${manifest.routes.length} routes) in ${pc.bold(formatDuration(duration))}`);
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
- console.log(` ${pc.red("✗")} ${error instanceof Error ? error.message : String(error)}`);
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 for development
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
  }>;
@@ -1,19 +1,23 @@
1
1
  /**
2
- * Dev command - Watch mode for development
2
+ * Dev command - Watch mode with LocalStack integration
3
3
  */
4
4
  import { defineCommand } from "citty";
5
- import consola from "consola";
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,352 @@ 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
- consola.start("Loading configuration...");
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
- consola.error(error instanceof Error ? error.message : error);
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
- consola.error(`Lambdas directory not found: ${lambdasDir}`);
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
- const lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
91
+ let lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
48
92
  if (lambdaFiles.length === 0) {
49
- consola.warn("No lambda files found in", config.lambdasDir);
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;
98
+ let lastError = null;
51
99
  // Build function for a single lambda
52
100
  async function buildSingleLambda(filename) {
53
- const name = filename.replace(/\.ts$/, "");
54
- const bundleName = getLambdaBundleName(name, name);
101
+ const lambdaName = filename.replace(/\.ts$/, "");
102
+ const bundleName = getLambdaBundleName(name, lambdaName);
55
103
  const inputPath = join(lambdasDir, filename);
56
104
  // Create wrapper entry
57
- const wrapperPath = join(tempDir, `${name}-wrapper.ts`);
105
+ const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
58
106
  const wrapperContent = createWrapperEntry(inputPath);
59
107
  await Bun.write(wrapperPath, wrapperContent);
60
108
  // Bundle with prefixed name
61
109
  const buildResult = await bundleLambda(bundleName, wrapperPath, outputDir, config);
62
110
  if (!buildResult.success) {
63
- consola.error(`Failed to build ${name}: ${buildResult.error}`);
64
111
  return false;
65
112
  }
66
- consola.success(`Built ${bundleName}.js`);
67
113
  // Package if not disabled
68
114
  if (!args["no-package"]) {
69
115
  const jsPath = join(outputDir, `${bundleName}.js`);
70
116
  const packageResult = await packageLambda(bundleName, jsPath, outputDir);
71
117
  if (!packageResult.success) {
72
- consola.error(`Failed to package ${name}: ${packageResult.error}`);
73
118
  return false;
74
119
  }
75
- consola.success(`Packaged ${bundleName}.zip`);
76
120
  }
77
121
  return true;
78
122
  }
79
- // Initial build of all lambdas
80
- consola.start("Initial build...");
81
- for (const file of lambdaFiles) {
82
- await buildSingleLambda(file);
123
+ // Build all lambdas (including OpenAPI if enabled)
124
+ async function buildAll() {
125
+ // Refresh lambda file list
126
+ lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
127
+ const filesToBuild = [...lambdaFiles];
128
+ // Generate OpenAPI aggregator if enabled
129
+ if (shouldGenerateOpenApi(config)) {
130
+ await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
131
+ filesToBuild.push("__openapi__.ts");
132
+ }
133
+ // Build all lambdas
134
+ for (const file of filesToBuild) {
135
+ const success = await buildSingleLambda(file);
136
+ if (!success) {
137
+ return { success: false, count: 0, error: `Failed to build ${file}` };
138
+ }
139
+ }
140
+ // Cleanup OpenAPI source file
141
+ if (shouldGenerateOpenApi(config)) {
142
+ await cleanupOpenApiLambda(lambdasDir);
143
+ }
144
+ return { success: true, count: filesToBuild.length };
83
145
  }
84
- consola.info("Initial build complete");
85
- console.log("");
86
- consola.box("Watch mode active\n\n" +
87
- `Watching: ${config.lambdasDir}/\n` +
88
- `Output: ${config.outputDir}/\n\n` +
89
- "Press Ctrl+C to stop");
90
- // Set up file watcher
91
- const watcher = watch(lambdasDir, { recursive: false }, async (event, filename) => {
92
- if (!filename || !filename.endsWith(".ts") || filename.startsWith("__")) {
146
+ // Generate manifest and terraform vars
147
+ async function generateManifestFiles() {
148
+ try {
149
+ const filesToManifest = [...lambdaFiles];
150
+ if (shouldGenerateOpenApi(config)) {
151
+ filesToManifest.push("__openapi__.ts");
152
+ }
153
+ const manifest = await generateManifest(filesToManifest, outputDir, config.openapi.enabled, name);
154
+ const manifestPath = join(outputDir, "manifest.json");
155
+ await writeManifest(manifest, manifestPath);
156
+ await writeTerraformVars(manifest, config);
157
+ return { success: true, routes: manifest.routes.length };
158
+ }
159
+ catch (err) {
160
+ return { success: false, routes: 0, error: err instanceof Error ? err.message : String(err) };
161
+ }
162
+ }
163
+ // Deploy to LocalStack
164
+ async function deployToLocalStack() {
165
+ const applyResult = await runTfLocalApply(terraformDir);
166
+ if (!applyResult.success) {
167
+ return { success: false, error: applyResult.error };
168
+ }
169
+ // Get and store outputs (transformed to LocalStack URLs)
170
+ lastOutputs = await getTerraformOutputs(terraformDir, true);
171
+ return { success: true };
172
+ }
173
+ // Show the final status screen (Vite-like)
174
+ function showReadyScreen(trigger, failed) {
175
+ ui.clear();
176
+ ui.header(pc.dim("dev"));
177
+ if (failed) {
178
+ ui.error("Build failed");
179
+ if (trigger) {
180
+ ui.info(`Triggered by: ${trigger}`);
181
+ }
182
+ if (lastError) {
183
+ ui.blank();
184
+ console.log(pc.dim(" Error:"));
185
+ // Show first few lines of error
186
+ const errorLines = lastError.split("\n").slice(0, 10);
187
+ for (const line of errorLines) {
188
+ console.log(pc.red(` ${line}`));
189
+ }
190
+ if (lastError.split("\n").length > 10) {
191
+ console.log(pc.dim(" ... (truncated)"));
192
+ }
193
+ }
194
+ }
195
+ else if (lastBuildInfo) {
196
+ ui.success(`Ready in ${pc.bold(formatDuration(lastBuildInfo.duration))}`);
197
+ ui.blank();
198
+ // Show outputs prominently
199
+ if (lastOutputs && Object.keys(lastOutputs).length > 0) {
200
+ for (const [key, value] of Object.entries(lastOutputs)) {
201
+ const valueStr = typeof value === "string" ? value : JSON.stringify(value);
202
+ console.log(` ${pc.dim("➜")} ${pc.bold(key)}: ${pc.cyan(valueStr)}`);
203
+ }
204
+ }
205
+ ui.blank();
206
+ console.log(pc.dim(` ${lastBuildInfo.lambdas} lambda${lastBuildInfo.lambdas === 1 ? "" : "s"} · ${lastBuildInfo.routes} routes`));
207
+ }
208
+ // Watch status
209
+ ui.blank();
210
+ console.log(pc.dim(" ─────────────────────────────────────"));
211
+ ui.blank();
212
+ const watchDirs = [config.lambdasDir];
213
+ if (localstackEnabled) {
214
+ watchDirs.push(config.terraform.outputDir);
215
+ }
216
+ console.log(` ${pc.dim("watching:")} ${watchDirs.join(", ")}`);
217
+ if (localstackEnabled) {
218
+ console.log(` ${pc.dim("deploy:")} ${pc.green("localstack")}`);
219
+ }
220
+ ui.blank();
221
+ console.log(pc.dim(" press ctrl+c to stop"));
222
+ ui.blank();
223
+ }
224
+ // Run the full build and deploy cycle
225
+ async function runBuildCycle(trigger) {
226
+ const cycleStart = Date.now();
227
+ lastError = null;
228
+ // Show building status
229
+ ui.clear();
230
+ ui.header(pc.dim("dev"));
231
+ if (trigger) {
232
+ ui.info(`Change: ${trigger}`);
233
+ ui.blank();
234
+ }
235
+ // Build
236
+ const buildSpinner = createSpinner("Building...").start();
237
+ const buildResult = await buildAll();
238
+ if (!buildResult.success) {
239
+ buildSpinner.fail("Build failed");
240
+ lastError = buildResult.error || "Unknown build error";
241
+ showReadyScreen(trigger, true);
242
+ return;
243
+ }
244
+ buildSpinner.succeed(`Built ${buildResult.count} lambda${buildResult.count === 1 ? "" : "s"}`);
245
+ // Generate manifest
246
+ const manifestSpinner = createSpinner("Generating manifest...").start();
247
+ const manifestResult = await generateManifestFiles();
248
+ if (!manifestResult.success) {
249
+ manifestSpinner.fail("Manifest failed");
250
+ lastError = manifestResult.error || "Unknown manifest error";
251
+ showReadyScreen(trigger, true);
252
+ return;
253
+ }
254
+ manifestSpinner.succeed("Generated manifest");
255
+ // Deploy if LocalStack enabled
256
+ if (localstackEnabled) {
257
+ const deploySpinner = createSpinner("Deploying...").start();
258
+ const deployResult = await deployToLocalStack();
259
+ if (!deployResult.success) {
260
+ deploySpinner.fail("Deploy failed");
261
+ lastError = deployResult.error || "Unknown deploy error";
262
+ showReadyScreen(trigger, true);
263
+ return;
264
+ }
265
+ deploySpinner.succeed("Deployed");
266
+ }
267
+ // Store build info
268
+ const duration = Date.now() - cycleStart;
269
+ lastBuildInfo = {
270
+ lambdas: buildResult.count,
271
+ routes: manifestResult.routes,
272
+ duration,
273
+ };
274
+ // Show ready screen
275
+ showReadyScreen(trigger);
276
+ }
277
+ // Run terraform-only deploy
278
+ async function runTerraformCycle(trigger) {
279
+ const cycleStart = Date.now();
280
+ lastError = null;
281
+ ui.clear();
282
+ ui.header(pc.dim("dev"));
283
+ ui.info(`Terraform: ${trigger}`);
284
+ ui.blank();
285
+ const deploySpinner = createSpinner("Deploying...").start();
286
+ const deployResult = await deployToLocalStack();
287
+ if (!deployResult.success) {
288
+ deploySpinner.fail("Deploy failed");
289
+ lastError = deployResult.error || "Unknown deploy error";
290
+ showReadyScreen(trigger, true);
93
291
  return;
94
292
  }
95
- console.log("");
96
- consola.info(`Change detected: ${filename}`);
97
- const success = await buildSingleLambda(filename);
98
- if (success) {
99
- consola.ready("Rebuild complete");
293
+ deploySpinner.succeed("Deployed");
294
+ // Update duration
295
+ if (lastBuildInfo) {
296
+ lastBuildInfo.duration = Date.now() - cycleStart;
100
297
  }
298
+ showReadyScreen(trigger);
299
+ }
300
+ // Debounce timer for file changes
301
+ let debounceTimer = null;
302
+ const DEBOUNCE_MS = 150;
303
+ // Pending changes during debounce
304
+ let pendingTrigger = null;
305
+ let pendingIsTerraform = false;
306
+ // Handle file change with debouncing
307
+ function handleFileChange(trigger, isTerraform = false) {
308
+ pendingTrigger = trigger;
309
+ pendingIsTerraform = isTerraform;
310
+ if (debounceTimer) {
311
+ clearTimeout(debounceTimer);
312
+ }
313
+ debounceTimer = setTimeout(async () => {
314
+ debounceTimer = null;
315
+ const triggerName = pendingTrigger || trigger;
316
+ const terraformOnly = pendingIsTerraform;
317
+ pendingTrigger = null;
318
+ pendingIsTerraform = false;
319
+ if (terraformOnly && localstackEnabled) {
320
+ await runTerraformCycle(triggerName);
321
+ }
322
+ else {
323
+ await runBuildCycle(triggerName);
324
+ }
325
+ }, DEBOUNCE_MS);
326
+ }
327
+ // Initial build
328
+ await runBuildCycle();
329
+ // Set up lambda file watcher
330
+ const lambdaWatcher = watch(lambdasDir, { recursive: false }, (event, filename) => {
331
+ if (!filename ||
332
+ !filename.endsWith(".ts") ||
333
+ filename.startsWith("__")) {
334
+ return;
335
+ }
336
+ handleFileChange(filename, false);
101
337
  });
338
+ // Set up terraform watcher if LocalStack is enabled
339
+ let terraformWatcher = null;
340
+ if (localstackEnabled && existsSync(terraformDir)) {
341
+ terraformWatcher = watch(terraformDir, { recursive: false }, (event, filename) => {
342
+ if (!filename)
343
+ return;
344
+ // Skip auto-generated files and tflocal override files
345
+ if (filename === config.terraform.tfvarsFilename ||
346
+ filename.endsWith(".auto.tfvars") ||
347
+ filename.startsWith(".terraform") ||
348
+ filename.endsWith(".tfstate") ||
349
+ filename.endsWith(".tfstate.backup") ||
350
+ filename.includes("override") ||
351
+ filename.startsWith("localstack")) {
352
+ return;
353
+ }
354
+ // Only watch .tf files
355
+ if (!filename.endsWith(".tf")) {
356
+ return;
357
+ }
358
+ handleFileChange(filename, true);
359
+ });
360
+ }
102
361
  // Handle graceful shutdown
103
362
  process.on("SIGINT", () => {
104
- console.log("");
105
- consola.info("Stopping watcher...");
106
- watcher.close();
363
+ ui.blank();
364
+ ui.info("Stopping...");
365
+ lambdaWatcher.close();
366
+ if (terraformWatcher) {
367
+ terraformWatcher.close();
368
+ }
369
+ // Clear debounce timer
370
+ if (debounceTimer) {
371
+ clearTimeout(debounceTimer);
372
+ }
107
373
  // Clean up temp directory
108
- const { rmSync } = require("fs");
109
374
  try {
110
375
  rmSync(tempDir, { recursive: true, force: true });
111
376
  }