@boltic/cli 1.0.38 → 1.0.40

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.
@@ -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
+ }