@boltic/cli 1.0.34 → 1.1.1-dev.1

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