@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,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
- consola.start("Loading configuration...");
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
- consola.error(error instanceof Error ? error.message : error);
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
- consola.error(`Build output directory not found: ${outputDir}\nRun 'elysian build' first.`);
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
- consola.error(`No built lambda files found in ${outputDir}\nRun 'elysian build' first.`);
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
- consola.info(`Found ${lambdaFiles.length} built lambda(s)`);
55
+ ui.success(`Found ${lambdaFiles.length} built lambda${lambdaFiles.length === 1 ? "" : "s"}`);
52
56
  // Generate manifest
53
- consola.start("Generating route manifest...");
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
- consola.success("Generated manifest.json");
63
+ ui.success("Generated manifest.json");
60
64
  // Write Terraform variables
61
- const tfvarsPath = await writeTerraformVars(manifest, config);
62
- consola.success(`Generated ${config.terraform.tfvarsFilename}`);
63
- // Print summary
64
- console.log("");
65
- consola.box(`Infrastructure files generated\n\n` +
66
- `Lambdas: ${manifest.lambdas.length}\n` +
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 params = route.pathParameters.length > 0
73
- ? ` [${route.pathParameters.join(", ")}]`
74
- : "";
75
- console.log(` ${route.method.padEnd(6)} ${route.path.padEnd(30)} ${route.lambda}${params}`);
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
- consola.error(error instanceof Error ? error.message : error);
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
- consola.start("Installing dependencies...");
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
- consola.success("Dependencies installed");
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
- console.log("");
188
+ ui.blank();
189
189
  for (const path of result.created) {
190
- consola.success(`Created ${getRelativePath(cwd, path)}`);
190
+ ui.success(`Created ${getRelativePath(cwd, path)}`);
191
191
  }
192
192
  for (const path of result.updated) {
193
- consola.success(`Updated ${getRelativePath(cwd, path)}`);
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("/hello", ({ query }) => {
23
- return \`Hello, \${query.name ?? "World"}!\`;
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
- consola.info("Cancelled");
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
- consola.success(`Created directory: ${targetDir}`);
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
- consola.error("elysian.config.ts already exists. Use --force to overwrite.");
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
- consola.info("Cancelled");
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
- consola.info("Cancelled");
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
- consola.error(error instanceof Error ? error.message : "Failed to install dependencies");
82
- consola.info(`You can manually install with: ${answers.packageManager} add elysia @actuallyjamez/elysian`);
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
- console.log("");
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
- const cdStep = targetDir !== "." ? `cd ${targetDir}\n` : "";
91
- console.log(` ${pc.green("✓")} Project initialized!`);
92
- console.log();
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
- console.log();
99
+ ui.blank();
101
100
  }
102
101
  console.log(` ${runCmd} elysian build`);
103
- console.log();
102
+ ui.blank();
104
103
  console.log(` cd terraform && terraform init && terraform apply`);
104
+ ui.blank();
105
105
  },
106
106
  });
@@ -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
+ }>;