@boltic/cli 1.0.39 → 1.0.41
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/README.md +123 -5
- package/api/serverless.js +174 -0
- package/cli.js +8 -0
- package/commands/serverless.js +1940 -0
- package/helper/serverless.js +1932 -0
- package/package.json +6 -3
|
@@ -0,0 +1,1932 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import TOML from "@iarna/toml";
|
|
6
|
+
import { execSync, spawn } from "child_process";
|
|
7
|
+
import {
|
|
8
|
+
uniqueNamesGenerator,
|
|
9
|
+
adjectives,
|
|
10
|
+
animals,
|
|
11
|
+
} from "unique-names-generator";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
|
|
14
|
+
// Supported languages and their versions
|
|
15
|
+
export const SUPPORTED_LANGUAGES = ["nodejs", "python", "golang", "java"];
|
|
16
|
+
export const LANGUAGE_VERSIONS = {
|
|
17
|
+
nodejs: "20",
|
|
18
|
+
python: "3",
|
|
19
|
+
golang: "1.22",
|
|
20
|
+
java: "17",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Handler mapping per language
|
|
24
|
+
export const HANDLER_MAPPING = {
|
|
25
|
+
nodejs: "handler.handler",
|
|
26
|
+
python: "index.handler",
|
|
27
|
+
golang: "handler.handler",
|
|
28
|
+
java: "Handler.handler",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Language display names for dropdown
|
|
32
|
+
export const LANGUAGE_CHOICES = [
|
|
33
|
+
{ name: "NodeJS", value: "nodejs" },
|
|
34
|
+
{ name: "Python", value: "python" },
|
|
35
|
+
{ name: "Golang", value: "golang" },
|
|
36
|
+
{ name: "Java", value: "java" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse command line arguments for the create command
|
|
41
|
+
*/
|
|
42
|
+
export function parseCreateArgs(args) {
|
|
43
|
+
const parsed = {
|
|
44
|
+
name: null,
|
|
45
|
+
language: null,
|
|
46
|
+
directory: process.cwd(),
|
|
47
|
+
type: null, // code, git, or container
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const arg = args[i];
|
|
52
|
+
const nextArg = args[i + 1];
|
|
53
|
+
|
|
54
|
+
if ((arg === "--name" || arg === "-n") && nextArg) {
|
|
55
|
+
parsed.name = nextArg;
|
|
56
|
+
i++;
|
|
57
|
+
} else if ((arg === "--language" || arg === "-l") && nextArg) {
|
|
58
|
+
parsed.language = nextArg.toLowerCase();
|
|
59
|
+
i++;
|
|
60
|
+
} else if ((arg === "--directory" || arg === "-d") && nextArg) {
|
|
61
|
+
parsed.directory = path.resolve(nextArg);
|
|
62
|
+
i++;
|
|
63
|
+
} else if ((arg === "--type" || arg === "-t") && nextArg) {
|
|
64
|
+
let typeValue = nextArg.toLowerCase();
|
|
65
|
+
// Map "blueprint" to "code" for UI consistency
|
|
66
|
+
if (typeValue === "blueprint") {
|
|
67
|
+
typeValue = "code";
|
|
68
|
+
}
|
|
69
|
+
parsed.type = typeValue;
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate a random serverless name using unique-names-generator
|
|
79
|
+
* Similar to go-randomdata's SillyName() function
|
|
80
|
+
* @see https://github.com/Pallinder/go-randomdata
|
|
81
|
+
*/
|
|
82
|
+
export function generateRandomName(language) {
|
|
83
|
+
const sillyName = uniqueNamesGenerator({
|
|
84
|
+
dictionaries: [adjectives, animals],
|
|
85
|
+
separator: "-",
|
|
86
|
+
length: 2,
|
|
87
|
+
style: "lowerCase",
|
|
88
|
+
});
|
|
89
|
+
return `${sillyName}-${language}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the boltic.yaml template content
|
|
94
|
+
*/
|
|
95
|
+
export function getBolticYamlContent(templateContext, language) {
|
|
96
|
+
return `app: "${templateContext.AppSlug}"
|
|
97
|
+
region: "${templateContext.Region}"
|
|
98
|
+
handler: "${HANDLER_MAPPING[language]}"
|
|
99
|
+
language: "${templateContext.Language}"
|
|
100
|
+
|
|
101
|
+
serverlessConfig:
|
|
102
|
+
Name: "${templateContext.AppSlug}"
|
|
103
|
+
Description: ""
|
|
104
|
+
Runtime: "code"
|
|
105
|
+
# Environment variables for your serverless function
|
|
106
|
+
# To add env variables, replace {} with key-value pairs like:
|
|
107
|
+
# Env:
|
|
108
|
+
# API_KEY: "your-api-key"
|
|
109
|
+
Env: {}
|
|
110
|
+
PortMap: []
|
|
111
|
+
Scaling:
|
|
112
|
+
AutoStop: false
|
|
113
|
+
Min: 1
|
|
114
|
+
Max: 1
|
|
115
|
+
MaxIdleTime: 300
|
|
116
|
+
Resources:
|
|
117
|
+
CPU: 0.1
|
|
118
|
+
MemoryMB: 128
|
|
119
|
+
MemoryMaxMB: 128
|
|
120
|
+
Timeout: 60
|
|
121
|
+
Validations: null
|
|
122
|
+
|
|
123
|
+
build:
|
|
124
|
+
builtin: dockerfile
|
|
125
|
+
ignorefile: .gitignore
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get handler file content based on language
|
|
131
|
+
*/
|
|
132
|
+
export function getHandlerContent(language) {
|
|
133
|
+
const handlers = {
|
|
134
|
+
nodejs: `// Define the handler function
|
|
135
|
+
export const handler = async (event, res) => {
|
|
136
|
+
try {
|
|
137
|
+
// Prepare the response JSON
|
|
138
|
+
const responseJson = {
|
|
139
|
+
message: "Hello World"
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Print the JSON response to stdout
|
|
143
|
+
console.log(JSON.stringify(responseJson));
|
|
144
|
+
|
|
145
|
+
// Set the response headers
|
|
146
|
+
res.setHeader('Content-Type', 'application/json');
|
|
147
|
+
|
|
148
|
+
// Send the response JSON
|
|
149
|
+
res.end(JSON.stringify(responseJson));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Handle errors
|
|
152
|
+
console.error(error);
|
|
153
|
+
// Send an error response if needed
|
|
154
|
+
res.statusCode = 500;
|
|
155
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
156
|
+
res.end('Internal Server Error');
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
`,
|
|
160
|
+
python: `# Import the required modules
|
|
161
|
+
from flask import jsonify
|
|
162
|
+
|
|
163
|
+
# Define the handler function
|
|
164
|
+
def handler(request):
|
|
165
|
+
# Create the response JSON
|
|
166
|
+
response_json = {
|
|
167
|
+
'message': 'Hello World'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Print the JSON response to stdout
|
|
171
|
+
print(response_json)
|
|
172
|
+
|
|
173
|
+
# Return a JSON response
|
|
174
|
+
return jsonify(response_json)
|
|
175
|
+
`,
|
|
176
|
+
golang: `package main
|
|
177
|
+
|
|
178
|
+
// Import necessary packages
|
|
179
|
+
import (
|
|
180
|
+
"encoding/json"
|
|
181
|
+
"fmt"
|
|
182
|
+
"net/http"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Define the handler function
|
|
186
|
+
func handler(req http.ResponseWriter, res *http.Request) {
|
|
187
|
+
// Initialize a map to hold the response body
|
|
188
|
+
response := map[string]string{
|
|
189
|
+
"message": "Hello, World!", // Set the message to "Hello, World!"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Set the Content-Type header to application/json
|
|
193
|
+
req.Header().Set("Content-Type", "application/json")
|
|
194
|
+
|
|
195
|
+
// Encode the response map into JSON and write it to the response
|
|
196
|
+
json.NewEncoder(req).Encode(response)
|
|
197
|
+
|
|
198
|
+
// Marshal the response map into JSON
|
|
199
|
+
responseJson, err := json.Marshal(response)
|
|
200
|
+
if err != nil {
|
|
201
|
+
http.Error(req, err.Error(), http.StatusInternalServerError)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Print the JSON response to stdout
|
|
206
|
+
fmt.Println(string(responseJson))
|
|
207
|
+
}
|
|
208
|
+
`,
|
|
209
|
+
java: `package com.boltic.io.serverless;
|
|
210
|
+
|
|
211
|
+
import org.springframework.http.ResponseEntity;
|
|
212
|
+
import org.springframework.stereotype.Service;
|
|
213
|
+
|
|
214
|
+
@Service
|
|
215
|
+
public class Handler {
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Handles the incoming request and returns a JSON response.
|
|
219
|
+
*
|
|
220
|
+
* @return ResponseEntity containing the JSON response or an error message.
|
|
221
|
+
*/
|
|
222
|
+
public ResponseEntity<String> handler(String method, String requestBody) {
|
|
223
|
+
try {
|
|
224
|
+
// Prepare the response JSON
|
|
225
|
+
String responseJson = "{\\"message\\": \\"Hello World\\"}";
|
|
226
|
+
|
|
227
|
+
// Print the JSON response to stdout
|
|
228
|
+
System.out.println(responseJson);
|
|
229
|
+
|
|
230
|
+
// Return the response with HTTP status 200 (OK)
|
|
231
|
+
return ResponseEntity.ok().body(responseJson);
|
|
232
|
+
} catch (Exception e) {
|
|
233
|
+
// Handle errors by printing the stack trace
|
|
234
|
+
e.printStackTrace();
|
|
235
|
+
|
|
236
|
+
// Return an error response with HTTP status 500 (Internal Server Error)
|
|
237
|
+
return ResponseEntity.status(500).body("Internal Server Error");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
`,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return handlers[language] || "";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the handler file path based on language
|
|
249
|
+
*/
|
|
250
|
+
export function getHandlerFilePath(language) {
|
|
251
|
+
const paths = {
|
|
252
|
+
nodejs: "handler.js",
|
|
253
|
+
python: "index.py",
|
|
254
|
+
golang: "handler.go",
|
|
255
|
+
java: "src/main/java/com/boltic/io/serverless/Handler.java",
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return paths[language] || "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create the serverless function files
|
|
263
|
+
*/
|
|
264
|
+
export function createServerlessFiles(targetDir, language, templateContext) {
|
|
265
|
+
// Create boltic.yaml
|
|
266
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
267
|
+
const bolticYamlContent = getBolticYamlContent(templateContext, language);
|
|
268
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
269
|
+
|
|
270
|
+
// Create handler file
|
|
271
|
+
const handlerRelativePath = getHandlerFilePath(language);
|
|
272
|
+
const handlerPath = path.join(targetDir, handlerRelativePath);
|
|
273
|
+
|
|
274
|
+
// Create directories if needed (for Java)
|
|
275
|
+
const handlerDir = path.dirname(handlerPath);
|
|
276
|
+
if (!fs.existsSync(handlerDir)) {
|
|
277
|
+
fs.mkdirSync(handlerDir, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const handlerContent = getHandlerContent(language);
|
|
281
|
+
fs.writeFileSync(handlerPath, handlerContent, "utf8");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Display success messages after serverless creation
|
|
286
|
+
*/
|
|
287
|
+
export function displayCreateSuccessMessages(targetDir) {
|
|
288
|
+
console.log("\n" + chalk.bgGreen.black(" ✓ SUCCESS ") + "\n");
|
|
289
|
+
|
|
290
|
+
console.log(
|
|
291
|
+
chalk.green("📁 Serverless function initialized at: ") +
|
|
292
|
+
chalk.cyan(targetDir)
|
|
293
|
+
);
|
|
294
|
+
console.log();
|
|
295
|
+
|
|
296
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
297
|
+
console.log();
|
|
298
|
+
|
|
299
|
+
console.log(chalk.yellow("📖 Next Steps:"));
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(
|
|
302
|
+
chalk.white(" 1. Navigate to your project directory:") +
|
|
303
|
+
chalk.cyan(` cd ${path.basename(targetDir)}`)
|
|
304
|
+
);
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.white(" 2. Test your function locally: ") +
|
|
307
|
+
chalk.cyan("boltic serverless test")
|
|
308
|
+
);
|
|
309
|
+
console.log(
|
|
310
|
+
chalk.white(" 3. Deploy your function by following the documentation")
|
|
311
|
+
);
|
|
312
|
+
console.log();
|
|
313
|
+
|
|
314
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
315
|
+
console.log();
|
|
316
|
+
|
|
317
|
+
console.log(chalk.blue("📚 Documentation:"));
|
|
318
|
+
console.log(
|
|
319
|
+
chalk.underline.cyan(
|
|
320
|
+
"https://docs.boltic.io/docs/compute/serverless/launch-your-application"
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// TEST COMMAND HELPERS
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
// Default handler files per language
|
|
331
|
+
export const DEFAULT_HANDLER_FILES = {
|
|
332
|
+
nodejs: "handler.js",
|
|
333
|
+
python: "index.py",
|
|
334
|
+
golang: "handler.go",
|
|
335
|
+
java: "src/main/java/com/boltic/io/serverless/Handler.java",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Generated wrapper file names
|
|
339
|
+
export const GENERATED_FILES = {
|
|
340
|
+
nodejs: ["autogen_index.js"],
|
|
341
|
+
python: ["autogen_index.py"],
|
|
342
|
+
golang: ["autogen_index.go", "go.mod"],
|
|
343
|
+
java: [
|
|
344
|
+
"src/main/java/com/boltic/io/serverless/AutogenIndex.java",
|
|
345
|
+
"pom.xml",
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Required dependencies per language
|
|
350
|
+
export const REQUIRED_DEPENDENCIES = {
|
|
351
|
+
nodejs: ["axios", "body-parser", "express@4.21.2", "nodemon", "winston"],
|
|
352
|
+
python: ["flask", "gunicorn", "waitress"],
|
|
353
|
+
golang: [],
|
|
354
|
+
java: [],
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Language detection files
|
|
358
|
+
const LANGUAGE_DETECTION_FILES = {
|
|
359
|
+
nodejs: ["package.json"],
|
|
360
|
+
python: ["requirements.txt", "pyproject.toml"],
|
|
361
|
+
golang: ["go.mod"],
|
|
362
|
+
java: ["pom.xml", "build.gradle"],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Parse command line arguments for the test command
|
|
367
|
+
*/
|
|
368
|
+
export function parseTestArgs(args) {
|
|
369
|
+
const parsed = {
|
|
370
|
+
port: 5555,
|
|
371
|
+
handlerFile: null,
|
|
372
|
+
handlerFunction: "handler",
|
|
373
|
+
language: null,
|
|
374
|
+
directory: process.cwd(),
|
|
375
|
+
command: null,
|
|
376
|
+
retain: false,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < args.length; i++) {
|
|
380
|
+
const arg = args[i];
|
|
381
|
+
const nextArg = args[i + 1];
|
|
382
|
+
|
|
383
|
+
if ((arg === "--port" || arg === "-p") && nextArg) {
|
|
384
|
+
parsed.port = parseInt(nextArg, 10);
|
|
385
|
+
i++;
|
|
386
|
+
} else if ((arg === "--handler-file" || arg === "-f") && nextArg) {
|
|
387
|
+
parsed.handlerFile = nextArg;
|
|
388
|
+
i++;
|
|
389
|
+
} else if ((arg === "--handler-function" || arg === "-u") && nextArg) {
|
|
390
|
+
parsed.handlerFunction = nextArg;
|
|
391
|
+
i++;
|
|
392
|
+
} else if ((arg === "--language" || arg === "-l") && nextArg) {
|
|
393
|
+
parsed.language = nextArg.toLowerCase();
|
|
394
|
+
i++;
|
|
395
|
+
} else if ((arg === "--directory" || arg === "-d") && nextArg) {
|
|
396
|
+
parsed.directory = path.resolve(nextArg);
|
|
397
|
+
i++;
|
|
398
|
+
} else if (arg === "--command" && nextArg) {
|
|
399
|
+
parsed.command = nextArg;
|
|
400
|
+
i++;
|
|
401
|
+
} else if (arg === "--retain" || arg === "-r") {
|
|
402
|
+
parsed.retain = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return parsed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Detect which config file exists in the directory
|
|
411
|
+
* Returns: { type: 'yaml' | 'toml' | null, path: string | null }
|
|
412
|
+
*/
|
|
413
|
+
export function detectBolticConfigFile(directory) {
|
|
414
|
+
const yamlPath = path.join(directory, "boltic.yaml");
|
|
415
|
+
const tomlPath = path.join(directory, "boltic.toml");
|
|
416
|
+
|
|
417
|
+
if (fs.existsSync(yamlPath)) {
|
|
418
|
+
return { type: "yaml", path: yamlPath };
|
|
419
|
+
}
|
|
420
|
+
if (fs.existsSync(tomlPath)) {
|
|
421
|
+
return { type: "toml", path: tomlPath };
|
|
422
|
+
}
|
|
423
|
+
return { type: null, path: null };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Load and parse boltic.yaml or boltic.toml configuration
|
|
428
|
+
* Supports both YAML and TOML formats
|
|
429
|
+
*/
|
|
430
|
+
export function loadBolticConfig(directory) {
|
|
431
|
+
const { type, path: configPath } = detectBolticConfigFile(directory);
|
|
432
|
+
|
|
433
|
+
if (!type || !configPath) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const configContent = fs.readFileSync(configPath, "utf8");
|
|
439
|
+
|
|
440
|
+
if (type === "yaml") {
|
|
441
|
+
const config = yaml.load(configContent);
|
|
442
|
+
return config;
|
|
443
|
+
} else if (type === "toml") {
|
|
444
|
+
const config = TOML.parse(configContent);
|
|
445
|
+
return config;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
const fileName = type === "yaml" ? "boltic.yaml" : "boltic.toml";
|
|
451
|
+
console.log(
|
|
452
|
+
chalk.yellow(
|
|
453
|
+
`⚠️ Warning: Could not parse ${fileName}: ${error.message}`
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Parse language from boltic.yaml language field (e.g., "nodejs/20" -> "nodejs")
|
|
462
|
+
*/
|
|
463
|
+
export function parseLanguageFromConfig(languageField) {
|
|
464
|
+
if (!languageField) return null;
|
|
465
|
+
// Handle format like "nodejs/20" or just "nodejs"
|
|
466
|
+
return languageField.split("/")[0].toLowerCase();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Parse handler config (e.g., "handler.handler" -> { file: "handler", function: "handler" })
|
|
471
|
+
*/
|
|
472
|
+
export function parseHandlerConfig(handlerField, language) {
|
|
473
|
+
if (!handlerField) {
|
|
474
|
+
return {
|
|
475
|
+
file: DEFAULT_HANDLER_FILES[language],
|
|
476
|
+
function: "handler",
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const parts = handlerField.split(".");
|
|
481
|
+
if (parts.length >= 2) {
|
|
482
|
+
const functionName = parts.pop();
|
|
483
|
+
const fileBase = parts.join(".");
|
|
484
|
+
|
|
485
|
+
// For Java, the handler file is in a specific package structure
|
|
486
|
+
if (language === "java") {
|
|
487
|
+
// Convert "Handler" to full path "src/main/java/com/boltic/io/serverless/Handler.java"
|
|
488
|
+
return {
|
|
489
|
+
file: `src/main/java/com/boltic/io/serverless/${fileBase}.java`,
|
|
490
|
+
function: functionName,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Add appropriate extension based on language
|
|
495
|
+
const extensions = {
|
|
496
|
+
nodejs: ".js",
|
|
497
|
+
python: ".py",
|
|
498
|
+
golang: ".go",
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
file: fileBase + (extensions[language] || ""),
|
|
503
|
+
function: functionName,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
file: DEFAULT_HANDLER_FILES[language],
|
|
509
|
+
function: handlerField,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Auto-detect language from project files
|
|
515
|
+
*/
|
|
516
|
+
export function detectLanguage(directory) {
|
|
517
|
+
console.log(chalk.cyan("🔍 Scanning source code..."));
|
|
518
|
+
|
|
519
|
+
for (const [language, files] of Object.entries(LANGUAGE_DETECTION_FILES)) {
|
|
520
|
+
for (const file of files) {
|
|
521
|
+
if (fs.existsSync(path.join(directory, file))) {
|
|
522
|
+
console.log(chalk.green(`✓ Detected ${language} app`));
|
|
523
|
+
return language;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get the wrapper file content for NodeJS
|
|
533
|
+
*/
|
|
534
|
+
function getNodeJSWrapperContent(handlerFile, handlerFunction) {
|
|
535
|
+
// Keep .js extension for ESM imports (Node.js requires it)
|
|
536
|
+
const importPath = "./" + handlerFile;
|
|
537
|
+
|
|
538
|
+
return `// autogen_index.js - System Generated File
|
|
539
|
+
// This file is automatically generated by the system.
|
|
540
|
+
// DO NOT EDIT - This file will be deleted after testing.
|
|
541
|
+
|
|
542
|
+
import express from 'express';
|
|
543
|
+
import bodyParser from 'body-parser';
|
|
544
|
+
import { ${handlerFunction} } from '${importPath}';
|
|
545
|
+
|
|
546
|
+
const PORT = process.env.BOLTIC_APPLICATION_PORT || 8080;
|
|
547
|
+
const DEV_MODE = process.env.BOLTIC_DEVELOPMENT_MODE || false;
|
|
548
|
+
|
|
549
|
+
const app = express();
|
|
550
|
+
|
|
551
|
+
app.disable('x-powered-by');
|
|
552
|
+
app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' }));
|
|
553
|
+
app.use(bodyParser.json({ limit: '25mb' }));
|
|
554
|
+
app.use(bodyParser.text({ limit: '25mb' }));
|
|
555
|
+
app.use(bodyParser.raw({ limit: '25mb' }));
|
|
556
|
+
|
|
557
|
+
const requestHandler = async (req, res) => {
|
|
558
|
+
try {
|
|
559
|
+
await ${handlerFunction}(req, res);
|
|
560
|
+
if (!res.headersSent) {
|
|
561
|
+
res.status(200).json({ message: "handler completed without sending a response" });
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.error("Error occurred while handling request:", error);
|
|
565
|
+
res.status(500).send("Internal Server Error");
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
app.all('*', requestHandler);
|
|
570
|
+
|
|
571
|
+
app.listen(PORT, () => {
|
|
572
|
+
if (DEV_MODE) {
|
|
573
|
+
console.log(\`Listening for events on port \${PORT} in development mode\`);
|
|
574
|
+
} else {
|
|
575
|
+
console.log(\`Listening for events\`);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Get the wrapper file content for Python
|
|
583
|
+
*/
|
|
584
|
+
function getPythonWrapperContent(handlerFile, handlerFunction) {
|
|
585
|
+
// Remove .py extension for import
|
|
586
|
+
const importModule = handlerFile.replace(/\.py$/, "");
|
|
587
|
+
|
|
588
|
+
return `# autogen_index.py - System Generated File
|
|
589
|
+
# This file is automatically generated by the system.
|
|
590
|
+
# DO NOT EDIT - This file will be deleted after testing.
|
|
591
|
+
|
|
592
|
+
from flask import Flask, request
|
|
593
|
+
from ${importModule} import ${handlerFunction}
|
|
594
|
+
import os
|
|
595
|
+
from waitress import serve
|
|
596
|
+
|
|
597
|
+
PORT = int(os.environ.get('BOLTIC_APPLICATION_PORT', 8080))
|
|
598
|
+
DEV_MODE = bool(os.environ.get('BOLTIC_DEVELOPMENT_MODE', False))
|
|
599
|
+
|
|
600
|
+
HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
|
|
601
|
+
|
|
602
|
+
app = Flask(__name__)
|
|
603
|
+
app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # 25 MB limit
|
|
604
|
+
|
|
605
|
+
@app.route('/', methods=HTTP_METHODS, defaults={'path': ''})
|
|
606
|
+
@app.route('/<path:path>', methods=HTTP_METHODS)
|
|
607
|
+
def index(path):
|
|
608
|
+
return ${handlerFunction}(request)
|
|
609
|
+
|
|
610
|
+
if __name__ == '__main__':
|
|
611
|
+
if DEV_MODE:
|
|
612
|
+
print('Listening for events on port {} in development mode'.format(PORT), flush=True)
|
|
613
|
+
else:
|
|
614
|
+
print('Listening for events', flush=True)
|
|
615
|
+
serve(app, host='0.0.0.0', port=PORT)
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get the wrapper file content for Golang
|
|
621
|
+
*/
|
|
622
|
+
function getGolangWrapperContent(handlerFunction) {
|
|
623
|
+
return `// autogen_index.go - System Generated File
|
|
624
|
+
// This file is automatically generated by the system.
|
|
625
|
+
// DO NOT EDIT - This file will be deleted after testing.
|
|
626
|
+
|
|
627
|
+
package main
|
|
628
|
+
|
|
629
|
+
import (
|
|
630
|
+
"fmt"
|
|
631
|
+
"log"
|
|
632
|
+
"net/http"
|
|
633
|
+
"os"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
func main() {
|
|
637
|
+
port := os.Getenv("BOLTIC_APPLICATION_PORT")
|
|
638
|
+
if port == "" {
|
|
639
|
+
port = "8080"
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
643
|
+
${handlerFunction}(w, r)
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
devMode := os.Getenv("BOLTIC_DEVELOPMENT_MODE")
|
|
647
|
+
if devMode == "true" {
|
|
648
|
+
fmt.Printf("Listening for events on port %s in development mode\\n", port)
|
|
649
|
+
} else {
|
|
650
|
+
fmt.Println("Listening for events")
|
|
651
|
+
}
|
|
652
|
+
log.Fatal(http.ListenAndServe(":"+port, nil))
|
|
653
|
+
}
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Get the wrapper file content for Java (Spring Boot)
|
|
659
|
+
*/
|
|
660
|
+
function getJavaWrapperContent(handlerFunction) {
|
|
661
|
+
return `// AutogenIndex.java - System Generated File
|
|
662
|
+
// This file is automatically generated by the system.
|
|
663
|
+
// DO NOT EDIT - This file will be deleted after testing.
|
|
664
|
+
|
|
665
|
+
package com.boltic.io.serverless;
|
|
666
|
+
|
|
667
|
+
import org.springframework.boot.SpringApplication;
|
|
668
|
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
669
|
+
import org.springframework.web.bind.annotation.*;
|
|
670
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
671
|
+
import org.springframework.http.ResponseEntity;
|
|
672
|
+
|
|
673
|
+
@SpringBootApplication
|
|
674
|
+
@RestController
|
|
675
|
+
public class AutogenIndex {
|
|
676
|
+
|
|
677
|
+
@Autowired
|
|
678
|
+
private Handler handler;
|
|
679
|
+
|
|
680
|
+
public static void main(String[] args) {
|
|
681
|
+
String devMode = System.getenv("BOLTIC_DEVELOPMENT_MODE");
|
|
682
|
+
String port = System.getenv("BOLTIC_APPLICATION_PORT");
|
|
683
|
+
if (port == null) port = "8080";
|
|
684
|
+
|
|
685
|
+
if ("true".equals(devMode)) {
|
|
686
|
+
System.out.println("Listening for events on port " + port + " in development mode");
|
|
687
|
+
} else {
|
|
688
|
+
System.out.println("Listening for events");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
SpringApplication.run(AutogenIndex.class, args);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@RequestMapping(value = "/**", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.PATCH})
|
|
695
|
+
public ResponseEntity<String> handleRequest(@RequestBody(required = false) String body, @RequestHeader java.util.Map<String, String> headers) {
|
|
696
|
+
return handler.${handlerFunction}(headers.getOrDefault("X-Http-Method", "GET"), body);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Get go.mod content for Golang projects (generated during test)
|
|
704
|
+
*/
|
|
705
|
+
function getGoModContent(appName) {
|
|
706
|
+
return `module ${appName}
|
|
707
|
+
|
|
708
|
+
go 1.22
|
|
709
|
+
`;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Get pom.xml content for Java projects (generated during test)
|
|
714
|
+
*/
|
|
715
|
+
function getJavaPomXmlContent(appName) {
|
|
716
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
717
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
718
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
719
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
720
|
+
<modelVersion>4.0.0</modelVersion>
|
|
721
|
+
|
|
722
|
+
<parent>
|
|
723
|
+
<groupId>org.springframework.boot</groupId>
|
|
724
|
+
<artifactId>spring-boot-starter-parent</artifactId>
|
|
725
|
+
<version>3.2.0</version>
|
|
726
|
+
<relativePath/>
|
|
727
|
+
</parent>
|
|
728
|
+
|
|
729
|
+
<groupId>com.boltic.io</groupId>
|
|
730
|
+
<artifactId>${appName}</artifactId>
|
|
731
|
+
<version>1.0.0</version>
|
|
732
|
+
<name>${appName}</name>
|
|
733
|
+
<description>Boltic Serverless Function</description>
|
|
734
|
+
|
|
735
|
+
<properties>
|
|
736
|
+
<java.version>17</java.version>
|
|
737
|
+
</properties>
|
|
738
|
+
|
|
739
|
+
<dependencies>
|
|
740
|
+
<dependency>
|
|
741
|
+
<groupId>org.springframework.boot</groupId>
|
|
742
|
+
<artifactId>spring-boot-starter-web</artifactId>
|
|
743
|
+
</dependency>
|
|
744
|
+
</dependencies>
|
|
745
|
+
|
|
746
|
+
<build>
|
|
747
|
+
<plugins>
|
|
748
|
+
<plugin>
|
|
749
|
+
<groupId>org.springframework.boot</groupId>
|
|
750
|
+
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
751
|
+
</plugin>
|
|
752
|
+
</plugins>
|
|
753
|
+
</build>
|
|
754
|
+
</project>
|
|
755
|
+
`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Detect the exported handler function name from the code
|
|
760
|
+
* This handles cases where the user might have renamed the function (e.g., handler -> handler1)
|
|
761
|
+
*/
|
|
762
|
+
export function detectHandlerFunctionFromCode(code, language) {
|
|
763
|
+
if (!code) return null;
|
|
764
|
+
|
|
765
|
+
switch (language) {
|
|
766
|
+
case "nodejs": {
|
|
767
|
+
// Match: export const <name> = or export function <name>( or export async function <name>(
|
|
768
|
+
const exportConstMatch = code.match(/export\s+const\s+(\w+)\s*=/);
|
|
769
|
+
const exportFunctionMatch = code.match(
|
|
770
|
+
/export\s+(async\s+)?function\s+(\w+)\s*\(/
|
|
771
|
+
);
|
|
772
|
+
const exportDefaultMatch = code.match(
|
|
773
|
+
/export\s+default\s+(async\s+)?function\s+(\w+)\s*\(/
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
if (exportConstMatch) return exportConstMatch[1];
|
|
777
|
+
if (exportFunctionMatch) return exportFunctionMatch[2];
|
|
778
|
+
if (exportDefaultMatch) return exportDefaultMatch[2];
|
|
779
|
+
|
|
780
|
+
// Also check for module.exports pattern
|
|
781
|
+
const moduleExportsMatch = code.match(
|
|
782
|
+
/module\.exports\s*=\s*{\s*(\w+)/
|
|
783
|
+
);
|
|
784
|
+
if (moduleExportsMatch) return moduleExportsMatch[1];
|
|
785
|
+
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
case "python": {
|
|
789
|
+
// Match: def <name>( at the start of a line (top-level function)
|
|
790
|
+
// We want to find the main handler function, typically the first or only def at top level
|
|
791
|
+
const defMatches = code.match(/^def\s+(\w+)\s*\(/gm);
|
|
792
|
+
if (defMatches && defMatches.length > 0) {
|
|
793
|
+
// Get the first function name
|
|
794
|
+
const firstMatch = defMatches[0].match(/^def\s+(\w+)\s*\(/);
|
|
795
|
+
if (firstMatch) return firstMatch[1];
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
case "golang": {
|
|
800
|
+
// Match: func <Name>( where Name starts with uppercase (exported)
|
|
801
|
+
// Look for handler-like functions that take http.ResponseWriter and *http.Request
|
|
802
|
+
const funcMatches = code.match(
|
|
803
|
+
/func\s+([A-Z]\w*)\s*\([^)]*http\.ResponseWriter/g
|
|
804
|
+
);
|
|
805
|
+
if (funcMatches && funcMatches.length > 0) {
|
|
806
|
+
const firstMatch = funcMatches[0].match(
|
|
807
|
+
/func\s+([A-Z]\w*)\s*\(/
|
|
808
|
+
);
|
|
809
|
+
if (firstMatch) return firstMatch[1];
|
|
810
|
+
}
|
|
811
|
+
// Fallback: any exported function (starts with uppercase)
|
|
812
|
+
const anyExportedMatch = code.match(/func\s+([A-Z]\w*)\s*\(/);
|
|
813
|
+
if (anyExportedMatch) return anyExportedMatch[1];
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
case "java": {
|
|
817
|
+
// Match: public <return_type> <name>( - typically looking for handler method
|
|
818
|
+
// Skip common methods like main, constructor
|
|
819
|
+
const methodMatches = code.match(
|
|
820
|
+
/public\s+\w+(?:<[^>]+>)?\s+(\w+)\s*\([^)]*\)/g
|
|
821
|
+
);
|
|
822
|
+
if (methodMatches) {
|
|
823
|
+
for (const match of methodMatches) {
|
|
824
|
+
const methodNameMatch = match.match(
|
|
825
|
+
/public\s+\w+(?:<[^>]+>)?\s+(\w+)\s*\(/
|
|
826
|
+
);
|
|
827
|
+
if (methodNameMatch) {
|
|
828
|
+
const methodName = methodNameMatch[1];
|
|
829
|
+
// Skip constructor, main, and common Spring methods
|
|
830
|
+
if (
|
|
831
|
+
!["main", "run", "configure", "init"].includes(
|
|
832
|
+
methodName.toLowerCase()
|
|
833
|
+
) &&
|
|
834
|
+
!methodName.match(/^[A-Z]/)
|
|
835
|
+
) {
|
|
836
|
+
// Skip if looks like constructor
|
|
837
|
+
return methodName;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
default:
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Generate wrapper file content based on language
|
|
851
|
+
*/
|
|
852
|
+
export function generateWrapperContent(language, handlerFile, handlerFunction) {
|
|
853
|
+
switch (language) {
|
|
854
|
+
case "nodejs":
|
|
855
|
+
return getNodeJSWrapperContent(handlerFile, handlerFunction);
|
|
856
|
+
case "python":
|
|
857
|
+
return getPythonWrapperContent(handlerFile, handlerFunction);
|
|
858
|
+
case "golang":
|
|
859
|
+
return getGolangWrapperContent(handlerFunction);
|
|
860
|
+
case "java":
|
|
861
|
+
return getJavaWrapperContent(handlerFunction);
|
|
862
|
+
default:
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Generate all test files for a language
|
|
869
|
+
* Returns an array of { path, content } objects
|
|
870
|
+
*/
|
|
871
|
+
export function generateTestFiles(
|
|
872
|
+
language,
|
|
873
|
+
handlerFile,
|
|
874
|
+
handlerFunction,
|
|
875
|
+
appName
|
|
876
|
+
) {
|
|
877
|
+
const files = [];
|
|
878
|
+
|
|
879
|
+
// Generate wrapper file
|
|
880
|
+
const wrapperContent = generateWrapperContent(
|
|
881
|
+
language,
|
|
882
|
+
handlerFile,
|
|
883
|
+
handlerFunction
|
|
884
|
+
);
|
|
885
|
+
if (wrapperContent) {
|
|
886
|
+
files.push({
|
|
887
|
+
path: GENERATED_FILES[language][0],
|
|
888
|
+
content: wrapperContent,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Generate additional files for Golang
|
|
893
|
+
if (language === "golang") {
|
|
894
|
+
files.push({
|
|
895
|
+
path: "go.mod",
|
|
896
|
+
content: getGoModContent(appName),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Generate additional files for Java
|
|
901
|
+
if (language === "java") {
|
|
902
|
+
files.push({
|
|
903
|
+
path: "pom.xml",
|
|
904
|
+
content: getJavaPomXmlContent(appName),
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return files;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Get the start command for the server based on language
|
|
913
|
+
*/
|
|
914
|
+
export function getStartCommand(language, directory, customCommand) {
|
|
915
|
+
if (customCommand) {
|
|
916
|
+
return {
|
|
917
|
+
command: customCommand.split(" ")[0],
|
|
918
|
+
args: customCommand.split(" ").slice(1),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Check if Python venv exists
|
|
923
|
+
const venvPython = path.join(directory, ".venv", "bin", "python3");
|
|
924
|
+
const usePythonVenv = fs.existsSync(venvPython);
|
|
925
|
+
|
|
926
|
+
const commands = {
|
|
927
|
+
nodejs: {
|
|
928
|
+
command: "npx",
|
|
929
|
+
args: ["nodemon", GENERATED_FILES.nodejs[0]],
|
|
930
|
+
},
|
|
931
|
+
python: {
|
|
932
|
+
command: usePythonVenv ? venvPython : "python3",
|
|
933
|
+
args: [GENERATED_FILES.python[0]],
|
|
934
|
+
},
|
|
935
|
+
golang: {
|
|
936
|
+
command: "go",
|
|
937
|
+
args: ["run", "."],
|
|
938
|
+
},
|
|
939
|
+
java: {
|
|
940
|
+
// Check if it's Maven or Gradle
|
|
941
|
+
// Use 'clean' to force fresh compilation (avoid stale class files)
|
|
942
|
+
command: fs.existsSync(path.join(directory, "pom.xml"))
|
|
943
|
+
? "mvn"
|
|
944
|
+
: "gradle",
|
|
945
|
+
args: fs.existsSync(path.join(directory, "pom.xml"))
|
|
946
|
+
? ["clean", "spring-boot:run", "-f", "pom.xml"]
|
|
947
|
+
: ["clean", "bootRun", "-b", "build.gradle"],
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
return commands[language] || { command: "", args: [] };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Check if NodeJS dependencies are installed
|
|
956
|
+
*/
|
|
957
|
+
export function checkNodeDependencies(directory, dependencies) {
|
|
958
|
+
const missingDeps = [];
|
|
959
|
+
|
|
960
|
+
for (const dep of dependencies) {
|
|
961
|
+
// Remove version specifier for checking
|
|
962
|
+
const depName = dep.split("@")[0];
|
|
963
|
+
const depPath = path.join(directory, "node_modules", depName);
|
|
964
|
+
|
|
965
|
+
if (!fs.existsSync(depPath)) {
|
|
966
|
+
missingDeps.push(dep);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return missingDeps;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Check if Python dependencies are installed
|
|
975
|
+
* Returns array of missing dependencies
|
|
976
|
+
*/
|
|
977
|
+
export function checkPythonDependencies(dependencies, execSync) {
|
|
978
|
+
const missingDeps = [];
|
|
979
|
+
|
|
980
|
+
for (const dep of dependencies) {
|
|
981
|
+
try {
|
|
982
|
+
execSync(`python3 -c "import ${dep}"`, { stdio: "ignore" });
|
|
983
|
+
} catch {
|
|
984
|
+
missingDeps.push(dep);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return missingDeps;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Get environment variables for the test server
|
|
993
|
+
*/
|
|
994
|
+
export function getTestEnvironmentVariables(port, language) {
|
|
995
|
+
const env = {
|
|
996
|
+
...process.env,
|
|
997
|
+
BOLTIC_DEVELOPMENT_MODE: "true",
|
|
998
|
+
BOLTIC_APPLICATION_PORT: String(port),
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Python-specific: disable output buffering
|
|
1002
|
+
if (language === "python") {
|
|
1003
|
+
env.PYTHONUNBUFFERED = "1";
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Java-specific: Spring Boot uses SERVER_PORT
|
|
1007
|
+
if (language === "java") {
|
|
1008
|
+
env.SERVER_PORT = String(port);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return env;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Clean up generated files
|
|
1016
|
+
*/
|
|
1017
|
+
export function cleanupGeneratedFiles(directory, language, retain) {
|
|
1018
|
+
if (retain) {
|
|
1019
|
+
console.log(
|
|
1020
|
+
chalk.yellow(
|
|
1021
|
+
"\n⚠️ Retaining auto-generated test files. Please delete them manually before deployment."
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const filesToDelete = GENERATED_FILES[language] || [];
|
|
1028
|
+
|
|
1029
|
+
console.log(chalk.cyan("\n🧹 Cleaning up generated files..."));
|
|
1030
|
+
|
|
1031
|
+
for (const file of filesToDelete) {
|
|
1032
|
+
const filePath = path.join(directory, file);
|
|
1033
|
+
if (fs.existsSync(filePath)) {
|
|
1034
|
+
try {
|
|
1035
|
+
fs.unlinkSync(filePath);
|
|
1036
|
+
console.log(chalk.dim(` Deleted: ${file}`));
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
console.log(
|
|
1039
|
+
chalk.yellow(
|
|
1040
|
+
` ⚠️ Could not delete ${file}: ${error.message}`
|
|
1041
|
+
)
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Display test server startup message
|
|
1050
|
+
*/
|
|
1051
|
+
export function displayTestStartupMessage(port) {
|
|
1052
|
+
console.log("\n" + chalk.bgCyan.black(" 🧪 LOCAL TEST SERVER ") + "\n");
|
|
1053
|
+
console.log(
|
|
1054
|
+
chalk.green("🚀 Starting local test server on ") +
|
|
1055
|
+
chalk.bold.cyan(`http://localhost:${port}`)
|
|
1056
|
+
);
|
|
1057
|
+
console.log();
|
|
1058
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
1059
|
+
console.log(chalk.dim(" Press Ctrl+C to stop the server"));
|
|
1060
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
1061
|
+
console.log();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
// PUBLISH COMMAND HELPERS
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Parse command line arguments for the publish command
|
|
1070
|
+
*/
|
|
1071
|
+
export function parsePublishArgs(args) {
|
|
1072
|
+
const parsed = {
|
|
1073
|
+
directory: process.cwd(),
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
for (let i = 0; i < args.length; i++) {
|
|
1077
|
+
const arg = args[i];
|
|
1078
|
+
const nextArg = args[i + 1];
|
|
1079
|
+
|
|
1080
|
+
if ((arg === "--directory" || arg === "-d") && nextArg) {
|
|
1081
|
+
parsed.directory = path.resolve(nextArg);
|
|
1082
|
+
i++;
|
|
1083
|
+
} else if (!arg.startsWith("-") && !parsed._dirSet) {
|
|
1084
|
+
// Accept positional argument as directory (e.g., `boltic serverless publish ./my-project`)
|
|
1085
|
+
parsed.directory = path.resolve(arg);
|
|
1086
|
+
parsed._dirSet = true;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
delete parsed._dirSet;
|
|
1091
|
+
return parsed;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Read handler file content based on language
|
|
1096
|
+
*/
|
|
1097
|
+
export function readHandlerFile(directory, language, config) {
|
|
1098
|
+
const handlerConfig = parseHandlerConfig(config?.handler, language);
|
|
1099
|
+
const handlerPath = path.join(directory, handlerConfig.file);
|
|
1100
|
+
|
|
1101
|
+
if (!fs.existsSync(handlerPath)) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return fs.readFileSync(handlerPath, "utf8");
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Build payload for updating an existing serverless function
|
|
1110
|
+
* Uses serverlessConfig from boltic.yaml
|
|
1111
|
+
* Only includes CodeOpts when runtime is "code"
|
|
1112
|
+
*/
|
|
1113
|
+
export function buildUpdatePayload(serverlessConfig, language, code) {
|
|
1114
|
+
const runtime = serverlessConfig?.Runtime || "code";
|
|
1115
|
+
|
|
1116
|
+
// Flatten PortMap if it's nested (e.g., [[{...}]] -> [{...}])
|
|
1117
|
+
let portMap = serverlessConfig?.PortMap || [];
|
|
1118
|
+
if (
|
|
1119
|
+
Array.isArray(portMap) &&
|
|
1120
|
+
portMap.length > 0 &&
|
|
1121
|
+
Array.isArray(portMap[0])
|
|
1122
|
+
) {
|
|
1123
|
+
portMap = portMap.flat();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const payload = {
|
|
1127
|
+
Name: serverlessConfig?.Name || "",
|
|
1128
|
+
Description: serverlessConfig?.Description || "",
|
|
1129
|
+
Runtime: runtime,
|
|
1130
|
+
Env: serverlessConfig?.Env || {},
|
|
1131
|
+
PortMap: runtime === "code" ? [] : portMap,
|
|
1132
|
+
Scaling: serverlessConfig?.Scaling || {
|
|
1133
|
+
AutoStop: false,
|
|
1134
|
+
Min: 1,
|
|
1135
|
+
Max: 1,
|
|
1136
|
+
MaxIdleTime: 300,
|
|
1137
|
+
},
|
|
1138
|
+
Resources: serverlessConfig?.Resources || {
|
|
1139
|
+
CPU: 0.1,
|
|
1140
|
+
MemoryMB: 128,
|
|
1141
|
+
MemoryMaxMB: 128,
|
|
1142
|
+
},
|
|
1143
|
+
Timeout: serverlessConfig?.Timeout || 60,
|
|
1144
|
+
Validations: serverlessConfig?.Validations || null,
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
// For code type: CodeOpts with Language, Packages, and Code
|
|
1148
|
+
if (runtime === "code") {
|
|
1149
|
+
payload.CodeOpts = {
|
|
1150
|
+
Language: language,
|
|
1151
|
+
Packages: [],
|
|
1152
|
+
Code: code,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// For git type: CodeOpts with Language and Packages only (no Code)
|
|
1157
|
+
if (runtime === "git") {
|
|
1158
|
+
payload.CodeOpts = {
|
|
1159
|
+
Language: language,
|
|
1160
|
+
Packages: [],
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// For container type: ContainerOpts only (no CodeOpts)
|
|
1165
|
+
if (runtime === "container") {
|
|
1166
|
+
payload.ContainerOpts = {
|
|
1167
|
+
Image: serverlessConfig?.ContainerOpts?.Image?.trim() || "",
|
|
1168
|
+
Args: serverlessConfig?.ContainerOpts?.Args || [],
|
|
1169
|
+
Command: serverlessConfig?.ContainerOpts?.Command || "",
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return payload;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Display publish success message
|
|
1178
|
+
*/
|
|
1179
|
+
export function displayPublishSuccessMessage(name, response) {
|
|
1180
|
+
const emoji = "🚀";
|
|
1181
|
+
|
|
1182
|
+
console.log(
|
|
1183
|
+
chalk.green(`${emoji} Serverless function PUBLISHED successfully!`)
|
|
1184
|
+
);
|
|
1185
|
+
console.log();
|
|
1186
|
+
console.log(chalk.cyan(" Name: ") + chalk.white(name));
|
|
1187
|
+
if (response?.ID) {
|
|
1188
|
+
console.log(chalk.cyan(" ID: ") + chalk.white(response.ID));
|
|
1189
|
+
}
|
|
1190
|
+
console.log();
|
|
1191
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
1192
|
+
console.log();
|
|
1193
|
+
console.log(chalk.blue("📚 Documentation:"));
|
|
1194
|
+
console.log(
|
|
1195
|
+
chalk.underline.cyan(
|
|
1196
|
+
"https://docs.boltic.io/docs/compute/serverless/launch-your-application"
|
|
1197
|
+
)
|
|
1198
|
+
);
|
|
1199
|
+
console.log();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// PULL COMMAND HELPERS
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Get boltic.yaml content for pulled serverless (includes serverlessId and serverlessConfig)
|
|
1206
|
+
*/
|
|
1207
|
+
export function getPulledBolticYamlContent(serverlessData) {
|
|
1208
|
+
const config = serverlessData.Config;
|
|
1209
|
+
const runtime = config.Runtime || "code";
|
|
1210
|
+
const language = config.CodeOpts?.Language || "nodejs/20";
|
|
1211
|
+
const handler =
|
|
1212
|
+
HANDLER_MAPPING[language.split("/")[0]] || "handler.handler";
|
|
1213
|
+
|
|
1214
|
+
// Flatten PortMap if nested
|
|
1215
|
+
let portMap = config.PortMap || [];
|
|
1216
|
+
if (
|
|
1217
|
+
Array.isArray(portMap) &&
|
|
1218
|
+
portMap.length > 0 &&
|
|
1219
|
+
Array.isArray(portMap[0])
|
|
1220
|
+
) {
|
|
1221
|
+
portMap = portMap.flat();
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Build serverlessConfig object
|
|
1225
|
+
const serverlessConfig = {
|
|
1226
|
+
Name: config.Name || "",
|
|
1227
|
+
Description: config.Description || "",
|
|
1228
|
+
Runtime: runtime,
|
|
1229
|
+
Env: config.Env || {},
|
|
1230
|
+
PortMap: portMap,
|
|
1231
|
+
Scaling: config.Scaling || {
|
|
1232
|
+
AutoStop: false,
|
|
1233
|
+
Min: 1,
|
|
1234
|
+
Max: 1,
|
|
1235
|
+
MaxIdleTime: 300,
|
|
1236
|
+
},
|
|
1237
|
+
Resources: config.Resources || {
|
|
1238
|
+
CPU: 0.1,
|
|
1239
|
+
MemoryMB: 128,
|
|
1240
|
+
MemoryMaxMB: 128,
|
|
1241
|
+
},
|
|
1242
|
+
Timeout: config.Timeout || 60,
|
|
1243
|
+
Validations: config.Validations || null,
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
// Format Env as YAML
|
|
1247
|
+
const envYaml =
|
|
1248
|
+
Object.keys(serverlessConfig.Env).length > 0
|
|
1249
|
+
? Object.entries(serverlessConfig.Env)
|
|
1250
|
+
.map(([key, value]) => ` ${key}: "${value}"`)
|
|
1251
|
+
.join("\n")
|
|
1252
|
+
: null;
|
|
1253
|
+
|
|
1254
|
+
// Format PortMap as YAML
|
|
1255
|
+
const portMapYaml =
|
|
1256
|
+
serverlessConfig.PortMap.length > 0
|
|
1257
|
+
? serverlessConfig.PortMap.map(
|
|
1258
|
+
(port) =>
|
|
1259
|
+
` - Name: "${port.Name || "port"}"\n Port: "${port.Port || "8080"}"\n Protocol: "${port.Protocol || "http"}"`
|
|
1260
|
+
).join("\n")
|
|
1261
|
+
: null;
|
|
1262
|
+
|
|
1263
|
+
// Check if ContainerOpts exists and has Image
|
|
1264
|
+
const containerOpts = config.ContainerOpts;
|
|
1265
|
+
const hasContainerOpts = containerOpts && containerOpts.Image;
|
|
1266
|
+
|
|
1267
|
+
// Format ContainerOpts as YAML if exists
|
|
1268
|
+
const containerOptsYaml = hasContainerOpts
|
|
1269
|
+
? ` ContainerOpts:
|
|
1270
|
+
Image: "${containerOpts.Image || ""}"
|
|
1271
|
+
Args: ${JSON.stringify(containerOpts.Args || [])}
|
|
1272
|
+
Command: "${containerOpts.Command || ""}"`
|
|
1273
|
+
: "";
|
|
1274
|
+
|
|
1275
|
+
// For container type, don't include handler and language
|
|
1276
|
+
const headerSection =
|
|
1277
|
+
runtime === "container"
|
|
1278
|
+
? `app: "${config.Name}"
|
|
1279
|
+
region: "${serverlessData.RegionID || "asia-south1"}"`
|
|
1280
|
+
: `app: "${config.Name}"
|
|
1281
|
+
region: "${serverlessData.RegionID || "asia-south1"}"
|
|
1282
|
+
handler: "${handler}"
|
|
1283
|
+
language: "${language}"`;
|
|
1284
|
+
|
|
1285
|
+
return `${headerSection}
|
|
1286
|
+
|
|
1287
|
+
serverlessConfig:
|
|
1288
|
+
serverlessId: "${serverlessData.ID}"
|
|
1289
|
+
Name: "${serverlessConfig.Name}"
|
|
1290
|
+
Description: "${serverlessConfig.Description}"
|
|
1291
|
+
Runtime: "${serverlessConfig.Runtime}"
|
|
1292
|
+
Env: ${envYaml ? `\n${envYaml}` : "{}"}
|
|
1293
|
+
PortMap: ${portMapYaml ? `\n${portMapYaml}` : "[]"}
|
|
1294
|
+
Scaling:
|
|
1295
|
+
AutoStop: ${serverlessConfig.Scaling.AutoStop}
|
|
1296
|
+
Min: ${serverlessConfig.Scaling.Min}
|
|
1297
|
+
Max: ${serverlessConfig.Scaling.Max}
|
|
1298
|
+
MaxIdleTime: ${serverlessConfig.Scaling.MaxIdleTime}
|
|
1299
|
+
Resources:
|
|
1300
|
+
CPU: ${serverlessConfig.Resources.CPU}
|
|
1301
|
+
MemoryMB: ${serverlessConfig.Resources.MemoryMB}
|
|
1302
|
+
MemoryMaxMB: ${serverlessConfig.Resources.MemoryMaxMB}
|
|
1303
|
+
Timeout: ${serverlessConfig.Timeout}
|
|
1304
|
+
Validations: ${serverlessConfig.Validations === null ? "null" : JSON.stringify(serverlessConfig.Validations)}
|
|
1305
|
+
${containerOptsYaml}
|
|
1306
|
+
|
|
1307
|
+
build:
|
|
1308
|
+
builtin: dockerfile
|
|
1309
|
+
ignorefile: .gitignore
|
|
1310
|
+
`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Build serverlessConfig object from API response data
|
|
1315
|
+
*/
|
|
1316
|
+
function buildServerlessConfigFromApi(serverlessData) {
|
|
1317
|
+
const config = serverlessData.Config;
|
|
1318
|
+
const runtime = config.Runtime || "code";
|
|
1319
|
+
|
|
1320
|
+
// Flatten PortMap if nested
|
|
1321
|
+
let portMap = config.PortMap || [];
|
|
1322
|
+
if (
|
|
1323
|
+
Array.isArray(portMap) &&
|
|
1324
|
+
portMap.length > 0 &&
|
|
1325
|
+
Array.isArray(portMap[0])
|
|
1326
|
+
) {
|
|
1327
|
+
portMap = portMap.flat();
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const serverlessConfig = {
|
|
1331
|
+
serverlessId: serverlessData.ID,
|
|
1332
|
+
Name: config.Name || "",
|
|
1333
|
+
Description: config.Description || "",
|
|
1334
|
+
Runtime: runtime,
|
|
1335
|
+
Env: config.Env || {},
|
|
1336
|
+
PortMap: portMap,
|
|
1337
|
+
Scaling: config.Scaling || {
|
|
1338
|
+
AutoStop: false,
|
|
1339
|
+
Min: 1,
|
|
1340
|
+
Max: 1,
|
|
1341
|
+
MaxIdleTime: 300,
|
|
1342
|
+
},
|
|
1343
|
+
Resources: config.Resources || {
|
|
1344
|
+
CPU: 0.1,
|
|
1345
|
+
MemoryMB: 128,
|
|
1346
|
+
MemoryMaxMB: 128,
|
|
1347
|
+
},
|
|
1348
|
+
Timeout: config.Timeout || 60,
|
|
1349
|
+
Validations: config.Validations || null,
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// Add ContainerOpts if present (for container type)
|
|
1353
|
+
if (config.ContainerOpts && config.ContainerOpts.Image) {
|
|
1354
|
+
serverlessConfig.ContainerOpts = {
|
|
1355
|
+
Image: config.ContainerOpts.Image || "",
|
|
1356
|
+
Args: config.ContainerOpts.Args || [],
|
|
1357
|
+
Command: config.ContainerOpts.Command || "",
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return serverlessConfig;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Update only serverlessConfig in existing boltic.yaml (for git type pull)
|
|
1366
|
+
* Preserves other fields like app, region, handler, language, build, etc.
|
|
1367
|
+
*/
|
|
1368
|
+
function updateBolticYamlServerlessConfig(bolticYamlPath, serverlessData) {
|
|
1369
|
+
try {
|
|
1370
|
+
// Read existing boltic.yaml
|
|
1371
|
+
const existingContent = fs.readFileSync(bolticYamlPath, "utf8");
|
|
1372
|
+
const existingConfig = yaml.load(existingContent);
|
|
1373
|
+
|
|
1374
|
+
// Build new serverlessConfig from API data
|
|
1375
|
+
const newServerlessConfig =
|
|
1376
|
+
buildServerlessConfigFromApi(serverlessData);
|
|
1377
|
+
|
|
1378
|
+
// Update only serverlessConfig, preserve everything else
|
|
1379
|
+
existingConfig.serverlessConfig = newServerlessConfig;
|
|
1380
|
+
|
|
1381
|
+
// Write back with preserved structure
|
|
1382
|
+
const updatedContent = yaml.dump(existingConfig, {
|
|
1383
|
+
indent: 2,
|
|
1384
|
+
lineWidth: -1,
|
|
1385
|
+
noRefs: true,
|
|
1386
|
+
quotingType: '"',
|
|
1387
|
+
forceQuotes: false,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
fs.writeFileSync(bolticYamlPath, updatedContent, "utf8");
|
|
1391
|
+
|
|
1392
|
+
console.log(
|
|
1393
|
+
chalk.green("✓ Updated serverlessConfig in existing boltic.yaml")
|
|
1394
|
+
);
|
|
1395
|
+
return true;
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
console.error(
|
|
1398
|
+
chalk.red("❌ Failed to update boltic.yaml:"),
|
|
1399
|
+
err.message
|
|
1400
|
+
);
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Update only serverlessConfig in existing boltic.toml (for git type pull)
|
|
1407
|
+
* Preserves other fields like app, region, handler, language, build, etc.
|
|
1408
|
+
*/
|
|
1409
|
+
function updateBolticTomlServerlessConfig(bolticTomlPath, serverlessData) {
|
|
1410
|
+
try {
|
|
1411
|
+
// Read existing boltic.toml
|
|
1412
|
+
const existingContent = fs.readFileSync(bolticTomlPath, "utf8");
|
|
1413
|
+
const existingConfig = TOML.parse(existingContent);
|
|
1414
|
+
|
|
1415
|
+
// Build new serverlessConfig from API data
|
|
1416
|
+
const newServerlessConfig =
|
|
1417
|
+
buildServerlessConfigFromApi(serverlessData);
|
|
1418
|
+
|
|
1419
|
+
// Update only serverlessConfig, preserve everything else
|
|
1420
|
+
existingConfig.serverlessConfig = newServerlessConfig;
|
|
1421
|
+
|
|
1422
|
+
// Write back with preserved structure
|
|
1423
|
+
const updatedContent = TOML.stringify(existingConfig);
|
|
1424
|
+
|
|
1425
|
+
fs.writeFileSync(bolticTomlPath, updatedContent, "utf8");
|
|
1426
|
+
|
|
1427
|
+
console.log(
|
|
1428
|
+
chalk.green("✓ Updated serverlessConfig in existing boltic.toml")
|
|
1429
|
+
);
|
|
1430
|
+
return true;
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
console.error(
|
|
1433
|
+
chalk.red("❌ Failed to update boltic.toml:"),
|
|
1434
|
+
err.message
|
|
1435
|
+
);
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Update serverlessConfig in existing boltic config file (yaml or toml)
|
|
1442
|
+
* Automatically detects file type and uses appropriate parser
|
|
1443
|
+
*/
|
|
1444
|
+
function updateBolticConfigServerlessConfig(targetDir, serverlessData) {
|
|
1445
|
+
const yamlPath = path.join(targetDir, "boltic.yaml");
|
|
1446
|
+
const tomlPath = path.join(targetDir, "boltic.toml");
|
|
1447
|
+
|
|
1448
|
+
if (fs.existsSync(yamlPath)) {
|
|
1449
|
+
return {
|
|
1450
|
+
updated: updateBolticYamlServerlessConfig(yamlPath, serverlessData),
|
|
1451
|
+
type: "yaml",
|
|
1452
|
+
path: yamlPath,
|
|
1453
|
+
};
|
|
1454
|
+
} else if (fs.existsSync(tomlPath)) {
|
|
1455
|
+
return {
|
|
1456
|
+
updated: updateBolticTomlServerlessConfig(tomlPath, serverlessData),
|
|
1457
|
+
type: "toml",
|
|
1458
|
+
path: tomlPath,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return { updated: false, type: null, path: null };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Handle git-type serverless pull - clone the repository
|
|
1467
|
+
*/
|
|
1468
|
+
function handleGitTypePull(targetDir, serverlessData) {
|
|
1469
|
+
const config = serverlessData.Config;
|
|
1470
|
+
|
|
1471
|
+
// Get git repository info from Links
|
|
1472
|
+
const gitRepo = serverlessData.Links?.Git?.Repository;
|
|
1473
|
+
const gitSshUrl = gitRepo?.SshURL;
|
|
1474
|
+
const gitHttpUrl = gitRepo?.CloneURL;
|
|
1475
|
+
const gitWebUrl = gitRepo?.HtmlURL;
|
|
1476
|
+
|
|
1477
|
+
if (!gitSshUrl && !gitHttpUrl) {
|
|
1478
|
+
console.log(
|
|
1479
|
+
chalk.yellow(
|
|
1480
|
+
"\n⚠️ No git repository URL found for this serverless."
|
|
1481
|
+
)
|
|
1482
|
+
);
|
|
1483
|
+
console.log(chalk.dim("Creating boltic.yaml only..."));
|
|
1484
|
+
|
|
1485
|
+
// Create boltic.yaml
|
|
1486
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
1487
|
+
const bolticYamlContent = getPulledBolticYamlContent(serverlessData);
|
|
1488
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
1489
|
+
|
|
1490
|
+
return { bolticConfigPath: bolticYamlPath, configType: "yaml" };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Check SSH access
|
|
1494
|
+
let hasGitAccess = false;
|
|
1495
|
+
console.log(chalk.cyan("\n🔍 Checking git repository access..."));
|
|
1496
|
+
|
|
1497
|
+
try {
|
|
1498
|
+
execSync(`git ls-remote ${gitSshUrl}`, {
|
|
1499
|
+
stdio: "pipe",
|
|
1500
|
+
timeout: 15000,
|
|
1501
|
+
});
|
|
1502
|
+
hasGitAccess = true;
|
|
1503
|
+
console.log(chalk.green("✓ SSH access verified!"));
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
hasGitAccess = false;
|
|
1506
|
+
console.log(chalk.yellow("⚠️ SSH access not available"));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (hasGitAccess) {
|
|
1510
|
+
// Clone the repository
|
|
1511
|
+
console.log(chalk.cyan("\n📥 Cloning repository..."));
|
|
1512
|
+
try {
|
|
1513
|
+
// Remove the target directory first (it was created empty)
|
|
1514
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1515
|
+
|
|
1516
|
+
// Clone into the target directory
|
|
1517
|
+
execSync(`git clone ${gitSshUrl} "${targetDir}"`, {
|
|
1518
|
+
stdio: "inherit",
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
console.log(chalk.green("\n✓ Repository cloned successfully!"));
|
|
1522
|
+
|
|
1523
|
+
// Check if boltic.yaml or boltic.toml already exists in the cloned repo
|
|
1524
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
1525
|
+
const bolticTomlPath = path.join(targetDir, "boltic.toml");
|
|
1526
|
+
|
|
1527
|
+
if (fs.existsSync(bolticYamlPath)) {
|
|
1528
|
+
// boltic.yaml exists - only update serverlessConfig with latest values
|
|
1529
|
+
console.log(
|
|
1530
|
+
chalk.cyan(
|
|
1531
|
+
"📋 Found existing boltic.yaml, updating serverlessConfig..."
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
updateBolticYamlServerlessConfig(
|
|
1535
|
+
bolticYamlPath,
|
|
1536
|
+
serverlessData
|
|
1537
|
+
);
|
|
1538
|
+
} else if (fs.existsSync(bolticTomlPath)) {
|
|
1539
|
+
// boltic.toml exists - only update serverlessConfig with latest values
|
|
1540
|
+
console.log(
|
|
1541
|
+
chalk.cyan(
|
|
1542
|
+
"📋 Found existing boltic.toml, updating serverlessConfig..."
|
|
1543
|
+
)
|
|
1544
|
+
);
|
|
1545
|
+
updateBolticTomlServerlessConfig(
|
|
1546
|
+
bolticTomlPath,
|
|
1547
|
+
serverlessData
|
|
1548
|
+
);
|
|
1549
|
+
return {
|
|
1550
|
+
bolticConfigPath: bolticTomlPath,
|
|
1551
|
+
configType: "toml",
|
|
1552
|
+
cloned: true,
|
|
1553
|
+
};
|
|
1554
|
+
} else {
|
|
1555
|
+
// Neither config file exists - create boltic.yaml from scratch
|
|
1556
|
+
console.log(chalk.cyan("📋 Creating boltic.yaml..."));
|
|
1557
|
+
const bolticYamlContent =
|
|
1558
|
+
getPulledBolticYamlContent(serverlessData);
|
|
1559
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
1560
|
+
console.log(chalk.green("✓ Created boltic.yaml"));
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return {
|
|
1564
|
+
bolticConfigPath: bolticYamlPath,
|
|
1565
|
+
configType: "yaml",
|
|
1566
|
+
cloned: true,
|
|
1567
|
+
};
|
|
1568
|
+
} catch (cloneErr) {
|
|
1569
|
+
console.error(
|
|
1570
|
+
chalk.red("\n❌ Failed to clone repository:"),
|
|
1571
|
+
cloneErr.message
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
// Recreate the directory and add boltic.yaml
|
|
1575
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1576
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
1577
|
+
const bolticYamlContent =
|
|
1578
|
+
getPulledBolticYamlContent(serverlessData);
|
|
1579
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
1580
|
+
|
|
1581
|
+
return {
|
|
1582
|
+
bolticConfigPath: bolticYamlPath,
|
|
1583
|
+
configType: "yaml",
|
|
1584
|
+
cloned: false,
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
} else {
|
|
1588
|
+
// No SSH access - show error and instructions
|
|
1589
|
+
console.log(
|
|
1590
|
+
chalk.red("\n❌ Cannot clone repository - SSH key not configured.")
|
|
1591
|
+
);
|
|
1592
|
+
console.log();
|
|
1593
|
+
console.log(
|
|
1594
|
+
chalk.yellow(
|
|
1595
|
+
"🔑 Please add your SSH key from Boltic Console → Settings → SSH Keys"
|
|
1596
|
+
)
|
|
1597
|
+
);
|
|
1598
|
+
console.log(chalk.yellow(" Then try pulling again."));
|
|
1599
|
+
|
|
1600
|
+
// Clean up the empty directory
|
|
1601
|
+
try {
|
|
1602
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
// Ignore cleanup errors
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return { error: true };
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Create folder structure for pulled serverless
|
|
1613
|
+
*/
|
|
1614
|
+
export function createPulledServerlessFiles(
|
|
1615
|
+
targetDir,
|
|
1616
|
+
serverlessData,
|
|
1617
|
+
serverlessType = "code"
|
|
1618
|
+
) {
|
|
1619
|
+
const config = serverlessData.Config;
|
|
1620
|
+
const language = config.CodeOpts?.Language?.split("/")[0] || "nodejs";
|
|
1621
|
+
|
|
1622
|
+
// Handle git-type serverless
|
|
1623
|
+
if (serverlessType === "git") {
|
|
1624
|
+
return handleGitTypePull(targetDir, serverlessData);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Handle container-type serverless - only create boltic.yaml
|
|
1628
|
+
if (serverlessType === "container") {
|
|
1629
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
1630
|
+
const bolticYamlContent = getPulledBolticYamlContent(serverlessData);
|
|
1631
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
1632
|
+
|
|
1633
|
+
console.log(chalk.green("✓ Created boltic.yaml"));
|
|
1634
|
+
return { bolticYamlPath };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// For code-type: Create boltic.yaml and handler file
|
|
1638
|
+
const bolticYamlPath = path.join(targetDir, "boltic.yaml");
|
|
1639
|
+
const bolticYamlContent = getPulledBolticYamlContent(serverlessData);
|
|
1640
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent, "utf8");
|
|
1641
|
+
|
|
1642
|
+
// Create handler file with the code from the serverless
|
|
1643
|
+
const handlerRelativePath = getHandlerFilePath(language);
|
|
1644
|
+
const handlerPath = path.join(targetDir, handlerRelativePath);
|
|
1645
|
+
|
|
1646
|
+
// Create directories if needed (for Java)
|
|
1647
|
+
const handlerDir = path.dirname(handlerPath);
|
|
1648
|
+
if (!fs.existsSync(handlerDir)) {
|
|
1649
|
+
fs.mkdirSync(handlerDir, { recursive: true });
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Use the code from the serverless or default handler content
|
|
1653
|
+
const handlerContent = config.CodeOpts?.Code || getHandlerContent(language);
|
|
1654
|
+
fs.writeFileSync(handlerPath, handlerContent, "utf8");
|
|
1655
|
+
|
|
1656
|
+
return {
|
|
1657
|
+
bolticYamlPath,
|
|
1658
|
+
handlerPath,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Display pull success message
|
|
1664
|
+
*/
|
|
1665
|
+
export function displayPullSuccessMessage(name, targetDir) {
|
|
1666
|
+
console.log("\n" + chalk.bgGreen.black(" ✓ PULLED ") + "\n");
|
|
1667
|
+
console.log(chalk.green("📥 Serverless function pulled successfully!"));
|
|
1668
|
+
console.log();
|
|
1669
|
+
console.log(chalk.cyan(" Name: ") + chalk.white(name));
|
|
1670
|
+
console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
|
|
1671
|
+
console.log();
|
|
1672
|
+
console.log(chalk.dim("━".repeat(60)));
|
|
1673
|
+
console.log();
|
|
1674
|
+
console.log(chalk.yellow("📖 Next Steps:"));
|
|
1675
|
+
console.log();
|
|
1676
|
+
console.log(
|
|
1677
|
+
chalk.white(" 1. Navigate to your project directory:") +
|
|
1678
|
+
chalk.cyan(` cd ${path.basename(targetDir)}`)
|
|
1679
|
+
);
|
|
1680
|
+
console.log(
|
|
1681
|
+
chalk.white(" 2. Test your function locally: ") +
|
|
1682
|
+
chalk.cyan("boltic serverless test")
|
|
1683
|
+
);
|
|
1684
|
+
console.log(
|
|
1685
|
+
chalk.white(" 3. Make changes and publish: ") +
|
|
1686
|
+
chalk.cyan("boltic serverless publish")
|
|
1687
|
+
);
|
|
1688
|
+
console.log();
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Run a Docker image locally.
|
|
1693
|
+
* @param {string} imageUri - Docker image URI (e.g., nginx:latest)
|
|
1694
|
+
* @param {object} options - Run options
|
|
1695
|
+
* @returns {Promise<void>}
|
|
1696
|
+
*/
|
|
1697
|
+
export function runDockerImage(imageUri, options = {}) {
|
|
1698
|
+
const {
|
|
1699
|
+
name = "test-container1",
|
|
1700
|
+
ports = [], // e.g. ["3000:3000"]
|
|
1701
|
+
envVars = {}, // { KEY: "value" }
|
|
1702
|
+
volumes = [], // e.g. ["./local:/app"]
|
|
1703
|
+
detach = false, // run in background
|
|
1704
|
+
} = options;
|
|
1705
|
+
|
|
1706
|
+
const args = ["run"];
|
|
1707
|
+
|
|
1708
|
+
if (detach) args.push("-d");
|
|
1709
|
+
if (name) args.push("--name", name);
|
|
1710
|
+
|
|
1711
|
+
ports.forEach((p) => args.push("-p", p));
|
|
1712
|
+
volumes.forEach((v) => args.push("-v", v));
|
|
1713
|
+
|
|
1714
|
+
Object.entries(envVars).forEach(([key, val]) => {
|
|
1715
|
+
args.push("-e", `${key}=${val}`);
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
args.push(imageUri);
|
|
1719
|
+
|
|
1720
|
+
console.log("Running:", ["docker", ...args].join(" "));
|
|
1721
|
+
|
|
1722
|
+
return new Promise((resolve, reject) => {
|
|
1723
|
+
const proc = spawn("docker", args, { stdio: "inherit" });
|
|
1724
|
+
|
|
1725
|
+
proc.on("error", reject);
|
|
1726
|
+
proc.on("exit", (code) => {
|
|
1727
|
+
if (code === 0) resolve();
|
|
1728
|
+
else reject(new Error(`Docker exited with code ${code}`));
|
|
1729
|
+
});
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Poll serverless status until it's running or timeout
|
|
1735
|
+
* @param {Function} fetchStatus - Function to fetch serverless status (pullServerless)
|
|
1736
|
+
* @param {string} serverlessId - ID of the serverless to poll
|
|
1737
|
+
* @param {Object} credentials - API credentials { apiUrl, token, accountId, session }
|
|
1738
|
+
* @param {number} maxTime - Maximum polling time in ms (default: 4 minutes)
|
|
1739
|
+
* @param {number} interval - Polling interval in ms (default: 20 seconds)
|
|
1740
|
+
*/
|
|
1741
|
+
export async function pollServerlessStatus(
|
|
1742
|
+
fetchStatus,
|
|
1743
|
+
serverlessId,
|
|
1744
|
+
credentials,
|
|
1745
|
+
maxTime = 8 * 60 * 1000,
|
|
1746
|
+
interval = 20 * 1000
|
|
1747
|
+
) {
|
|
1748
|
+
const { apiUrl, token, accountId, session } = credentials;
|
|
1749
|
+
const startTime = Date.now();
|
|
1750
|
+
let initialBuildId = null;
|
|
1751
|
+
let sawNewBuild = false;
|
|
1752
|
+
|
|
1753
|
+
// Format elapsed time nicely
|
|
1754
|
+
const formatElapsed = (ms) => {
|
|
1755
|
+
const seconds = Math.floor(ms / 1000);
|
|
1756
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1757
|
+
const minutes = Math.floor(seconds / 60);
|
|
1758
|
+
const remainingSecs = seconds % 60;
|
|
1759
|
+
return `${minutes}m ${remainingSecs}s`;
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// Get status text (without chalk for ora)
|
|
1763
|
+
const getStatusText = (status) => {
|
|
1764
|
+
switch (status) {
|
|
1765
|
+
case "running":
|
|
1766
|
+
return "Running";
|
|
1767
|
+
case "building":
|
|
1768
|
+
return "Building";
|
|
1769
|
+
case "pending":
|
|
1770
|
+
return "Pending";
|
|
1771
|
+
case "draft":
|
|
1772
|
+
return "Initializing";
|
|
1773
|
+
case "failed":
|
|
1774
|
+
case "error":
|
|
1775
|
+
return "Failed";
|
|
1776
|
+
case "success":
|
|
1777
|
+
case "successful":
|
|
1778
|
+
return "Success";
|
|
1779
|
+
default:
|
|
1780
|
+
return status || "Waiting";
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
console.log();
|
|
1785
|
+
console.log(chalk.cyan(" Deploying serverless function..."));
|
|
1786
|
+
console.log(
|
|
1787
|
+
chalk.dim(" Press Ctrl+C to exit. Check status later with: ") +
|
|
1788
|
+
chalk.white("boltic serverless status -n <name>")
|
|
1789
|
+
);
|
|
1790
|
+
console.log();
|
|
1791
|
+
|
|
1792
|
+
// Track current status for display
|
|
1793
|
+
let status = "pending";
|
|
1794
|
+
let buildStatus = null;
|
|
1795
|
+
let lastServerless = null;
|
|
1796
|
+
|
|
1797
|
+
// Create ora spinner
|
|
1798
|
+
const spinner = ora({
|
|
1799
|
+
text: `${getStatusText(status)} (0s)`,
|
|
1800
|
+
spinner: "dots",
|
|
1801
|
+
color: "cyan",
|
|
1802
|
+
}).start();
|
|
1803
|
+
|
|
1804
|
+
// Update elapsed time every second
|
|
1805
|
+
const timerInterval = setInterval(() => {
|
|
1806
|
+
const elapsed = Date.now() - startTime;
|
|
1807
|
+
let text = getStatusText(status);
|
|
1808
|
+
if (buildStatus && buildStatus !== status) {
|
|
1809
|
+
text += ` • Build: ${getStatusText(buildStatus)}`;
|
|
1810
|
+
}
|
|
1811
|
+
text += ` (${formatElapsed(elapsed)})`;
|
|
1812
|
+
spinner.text = text;
|
|
1813
|
+
}, 1000);
|
|
1814
|
+
|
|
1815
|
+
// Cleanup function
|
|
1816
|
+
const cleanup = () => {
|
|
1817
|
+
clearInterval(timerInterval);
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
try {
|
|
1821
|
+
while (Date.now() - startTime < maxTime) {
|
|
1822
|
+
try {
|
|
1823
|
+
// Fetch serverless status
|
|
1824
|
+
const serverless = await fetchStatus(
|
|
1825
|
+
apiUrl,
|
|
1826
|
+
token,
|
|
1827
|
+
accountId,
|
|
1828
|
+
session,
|
|
1829
|
+
serverlessId
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
lastServerless = serverless;
|
|
1833
|
+
status = serverless?.Status || "pending";
|
|
1834
|
+
buildStatus =
|
|
1835
|
+
serverless?.LastBuild?.StatusHistory?.slice(-1)[0]?.Status;
|
|
1836
|
+
const currentBuildId = serverless?.LastBuild?.ID;
|
|
1837
|
+
|
|
1838
|
+
// Track if a new build has started
|
|
1839
|
+
if (!initialBuildId) {
|
|
1840
|
+
initialBuildId = currentBuildId;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Detect new build
|
|
1844
|
+
if (
|
|
1845
|
+
currentBuildId !== initialBuildId ||
|
|
1846
|
+
buildStatus === "building" ||
|
|
1847
|
+
buildStatus === "created"
|
|
1848
|
+
) {
|
|
1849
|
+
sawNewBuild = true;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Update spinner color based on status
|
|
1853
|
+
if (status === "running" || buildStatus === "success") {
|
|
1854
|
+
spinner.color = "green";
|
|
1855
|
+
} else if (status === "failed" || buildStatus === "failed") {
|
|
1856
|
+
spinner.color = "red";
|
|
1857
|
+
} else if (
|
|
1858
|
+
status === "building" ||
|
|
1859
|
+
buildStatus === "building"
|
|
1860
|
+
) {
|
|
1861
|
+
spinner.color = "yellow";
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Success condition
|
|
1865
|
+
const isBuildComplete =
|
|
1866
|
+
buildStatus === "success" || buildStatus === "successful";
|
|
1867
|
+
const isRunning = status === "running";
|
|
1868
|
+
|
|
1869
|
+
if (sawNewBuild && isRunning && isBuildComplete) {
|
|
1870
|
+
const elapsed = Date.now() - startTime;
|
|
1871
|
+
cleanup();
|
|
1872
|
+
spinner.succeed(
|
|
1873
|
+
chalk.green(
|
|
1874
|
+
`Deployed successfully in ${formatElapsed(elapsed)}`
|
|
1875
|
+
)
|
|
1876
|
+
);
|
|
1877
|
+
console.log();
|
|
1878
|
+
|
|
1879
|
+
// Print access URL if available
|
|
1880
|
+
const appDomain = serverless?.AppDomain?.[0];
|
|
1881
|
+
if (appDomain) {
|
|
1882
|
+
const url = `https://${appDomain.DomainName}.${appDomain.BaseUrl || "serverless.boltic.app"}`;
|
|
1883
|
+
console.log(
|
|
1884
|
+
chalk.cyan(" 🌐 Your serverless is live at:")
|
|
1885
|
+
);
|
|
1886
|
+
console.log(chalk.white.bold(` ${url}`));
|
|
1887
|
+
console.log();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
return { success: true, status, serverless };
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Check for failed status
|
|
1894
|
+
if (status === "failed" || buildStatus === "failed") {
|
|
1895
|
+
const elapsed = Date.now() - startTime;
|
|
1896
|
+
cleanup();
|
|
1897
|
+
spinner.fail(
|
|
1898
|
+
chalk.red(
|
|
1899
|
+
`Deployment failed after ${formatElapsed(elapsed)}`
|
|
1900
|
+
)
|
|
1901
|
+
);
|
|
1902
|
+
console.log();
|
|
1903
|
+
return { success: false, status, serverless };
|
|
1904
|
+
}
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
// Error during fetch - will retry on next poll
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// Wait before next poll
|
|
1910
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Timeout reached
|
|
1914
|
+
cleanup();
|
|
1915
|
+
spinner.warn(chalk.yellow(`Timeout after ${formatElapsed(maxTime)}`));
|
|
1916
|
+
console.log(chalk.dim(" Deployment may still be in progress."));
|
|
1917
|
+
console.log(
|
|
1918
|
+
chalk.dim(" Check status: boltic serverless status -n <name>")
|
|
1919
|
+
);
|
|
1920
|
+
console.log();
|
|
1921
|
+
|
|
1922
|
+
return {
|
|
1923
|
+
success: false,
|
|
1924
|
+
status: "timeout",
|
|
1925
|
+
serverless: lastServerless,
|
|
1926
|
+
};
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
cleanup();
|
|
1929
|
+
spinner.fail("Polling interrupted");
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
}
|