@boltic/cli 1.0.35 → 1.1.1-dev.2

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