@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,1958 @@
1
+ import { search, input } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { spawn, execSync } from "child_process";
6
+
7
+ import { getCurrentEnv } from "../helper/env.js";
8
+ import {
9
+ SUPPORTED_LANGUAGES,
10
+ LANGUAGE_VERSIONS,
11
+ HANDLER_MAPPING,
12
+ LANGUAGE_CHOICES,
13
+ REQUIRED_DEPENDENCIES,
14
+ parseCreateArgs,
15
+ parseTestArgs,
16
+ parsePublishArgs,
17
+ createServerlessFiles,
18
+ loadBolticConfig,
19
+ parseLanguageFromConfig,
20
+ parseHandlerConfig,
21
+ detectLanguage,
22
+ generateTestFiles,
23
+ getStartCommand,
24
+ checkNodeDependencies,
25
+ getTestEnvironmentVariables,
26
+ cleanupGeneratedFiles,
27
+ displayTestStartupMessage,
28
+ readHandlerFile,
29
+ buildUpdatePayload,
30
+ displayPublishSuccessMessage,
31
+ createPulledServerlessFiles,
32
+ displayPullSuccessMessage,
33
+ detectHandlerFunctionFromCode,
34
+ pollServerlessStatus,
35
+ } from "../helper/serverless.js";
36
+ import {
37
+ listAllServerless,
38
+ pullServerless,
39
+ publishServerless,
40
+ updateServerless,
41
+ } from "../api/serverless.js";
42
+
43
+ // Define commands and their descriptions
44
+ const commands = {
45
+ create: {
46
+ description: "Create a new serverless function",
47
+ action: handleCreate,
48
+ },
49
+ publish: {
50
+ description: "Publish a serverless",
51
+ action: handlePublish,
52
+ },
53
+ pull: {
54
+ description: "Pull a serverless",
55
+ action: handlePull,
56
+ },
57
+ test: {
58
+ description: "Test a serverless function locally",
59
+ action: handleTest,
60
+ },
61
+ help: {
62
+ description: "Show help for serverless commands",
63
+ action: showHelp,
64
+ },
65
+ list: {
66
+ description: "List all serverless functions",
67
+ action: handleList,
68
+ },
69
+ status: {
70
+ description: "Show status of a serverless function",
71
+ action: handleStatus,
72
+ },
73
+ };
74
+
75
+ // Serverless type choices for dropdown
76
+ const SERVERLESS_TYPE_CHOICES = [
77
+ { name: "šŸ“¦ Git - Deploy from Git repository", value: "git" },
78
+ { name: "šŸ“ Blueprint - Write code directly", value: "code" },
79
+ { name: "🐳 Container - Deploy Docker container", value: "container" },
80
+ ];
81
+
82
+ /**
83
+ * Handle the create serverless command
84
+ */
85
+ async function handleCreate(args = []) {
86
+ try {
87
+ console.log(
88
+ "\n" +
89
+ chalk.bgCyan.black(" šŸš€ SERVERLESS CREATE ") +
90
+ chalk.cyan(" Initialize a new serverless function\n")
91
+ );
92
+
93
+ // Step 1: Parse CLI arguments
94
+ const parsedArgs = parseCreateArgs(args);
95
+ let { name, language, directory, type } = parsedArgs;
96
+
97
+ // Step 2: Serverless Type Selection
98
+ if (!type) {
99
+ type = await search({
100
+ message: "Select Serverless Type:",
101
+ source: async (term) => {
102
+ if (!term) return SERVERLESS_TYPE_CHOICES;
103
+ return SERVERLESS_TYPE_CHOICES.filter(
104
+ (choice) =>
105
+ choice.name
106
+ .toLowerCase()
107
+ .includes(term.toLowerCase()) ||
108
+ choice.value
109
+ .toLowerCase()
110
+ .includes(term.toLowerCase())
111
+ );
112
+ },
113
+ });
114
+ }
115
+
116
+ console.log(chalk.cyan("šŸ“¦ Selected type: ") + chalk.bold.white(type));
117
+
118
+ // Step 3: Name Input (required - no random generation)
119
+ if (!name) {
120
+ name = await input({
121
+ message: "Enter serverless function name:",
122
+ validate: (value) => {
123
+ if (!value || value.trim() === "") {
124
+ return "Name is required";
125
+ }
126
+ // Validate name format (alphanumeric, hyphens, underscores)
127
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value.trim())) {
128
+ return "Name must start with a letter and contain only letters, numbers, hyphens, and underscores";
129
+ }
130
+ return true;
131
+ },
132
+ });
133
+ name = name.trim();
134
+ }
135
+ console.log(
136
+ chalk.cyan("šŸ“› Serverless name: ") + chalk.bold.white(name)
137
+ );
138
+
139
+ // Step 4: Language Selection (skip for container type)
140
+ let version = null;
141
+ if (type !== "container") {
142
+ if (!language) {
143
+ language = await search({
144
+ message: "Select Language:",
145
+ source: async (term) => {
146
+ if (!term) return LANGUAGE_CHOICES;
147
+ return LANGUAGE_CHOICES.filter(
148
+ (choice) =>
149
+ choice.name
150
+ .toLowerCase()
151
+ .includes(term.toLowerCase()) ||
152
+ choice.value
153
+ .toLowerCase()
154
+ .includes(term.toLowerCase())
155
+ );
156
+ },
157
+ });
158
+ } else {
159
+ // Validate the provided language
160
+ if (!SUPPORTED_LANGUAGES.includes(language)) {
161
+ console.error(
162
+ chalk.red(`\nāŒ Unsupported language: ${language}`)
163
+ );
164
+ console.log(
165
+ chalk.yellow(
166
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
167
+ )
168
+ );
169
+ return;
170
+ }
171
+ }
172
+
173
+ // Step 5: Get latest language version
174
+ version = LANGUAGE_VERSIONS[language];
175
+ }
176
+
177
+ // Step 6: Determine target directory
178
+ const targetDir = path.join(directory, name);
179
+
180
+ // Check if directory already exists
181
+ if (fs.existsSync(targetDir)) {
182
+ console.error(
183
+ chalk.red(`\nāŒ Directory already exists: ${targetDir}`)
184
+ );
185
+ console.log(
186
+ chalk.yellow(
187
+ "Please choose a different name or delete the existing directory."
188
+ )
189
+ );
190
+ return;
191
+ }
192
+
193
+ // Create the target directory
194
+ try {
195
+ fs.mkdirSync(targetDir, { recursive: true });
196
+ } catch (err) {
197
+ console.error(
198
+ chalk.red(`\nāŒ Failed to create directory: ${targetDir}`)
199
+ );
200
+ console.error(chalk.red(`Error: ${err.message}`));
201
+ return;
202
+ }
203
+
204
+ // Branch based on type
205
+ if (type === "git") {
206
+ // For git type: create empty folder with boltic-properties.yaml only
207
+ await handleGitTypeCreate(name, language, version, targetDir);
208
+ return;
209
+ }
210
+
211
+ if (type === "container") {
212
+ // For container type: ask for image and create serverless
213
+ await handleContainerTypeCreate(name, targetDir);
214
+ return;
215
+ }
216
+
217
+ // For code type: create full template files and call create API
218
+ await handleCodeTypeCreate(name, language, version, targetDir);
219
+ } catch (error) {
220
+ if (
221
+ error.message &&
222
+ error.message.includes("User force closed the prompt")
223
+ ) {
224
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
225
+ return;
226
+ }
227
+ // Handle other errors
228
+ console.error(
229
+ chalk.red("\nāŒ An error occurred:"),
230
+ error.message || "Unknown error"
231
+ );
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Handle code type serverless creation - creates folder with template files and calls create API
237
+ */
238
+ async function handleCodeTypeCreate(name, language, version, targetDir) {
239
+ const templateContext = {
240
+ AppSlug: name,
241
+ Language: `${language}/${version}`,
242
+ Region: "asia-south1",
243
+ };
244
+
245
+ console.log(chalk.cyan("\nšŸ“ Creating serverless function files..."));
246
+ console.log(chalk.dim(` Type: code`));
247
+ console.log(chalk.dim(` Language: ${language}/${version}`));
248
+ console.log(chalk.dim(` Region: ${templateContext.Region}`));
249
+ console.log(chalk.dim(` Handler: ${HANDLER_MAPPING[language]}`));
250
+
251
+ // Create template files
252
+ try {
253
+ createServerlessFiles(targetDir, language, templateContext);
254
+ } catch (err) {
255
+ console.error(chalk.red(`\nāŒ Failed to create template files`));
256
+ console.error(chalk.red(`Error: ${err.message}`));
257
+ // Cleanup
258
+ try {
259
+ fs.rmSync(targetDir, { recursive: true, force: true });
260
+ } catch {
261
+ // Ignore cleanup errors
262
+ }
263
+ return;
264
+ }
265
+
266
+ // Get authentication credentials
267
+ const env = await getCurrentEnv();
268
+ if (!env || !env.token || !env.session) {
269
+ console.error(chalk.red("\nāŒ Not authenticated. Please login first."));
270
+ console.log(chalk.yellow(" Run: boltic login"));
271
+ return;
272
+ }
273
+
274
+ const { apiUrl, token, accountId, session } = env;
275
+
276
+ // Read the handler file to get the code
277
+ const handlerFileName = HANDLER_MAPPING[language].split(".")[0];
278
+ let handlerFile;
279
+ if (language === "java") {
280
+ handlerFile = path.join(
281
+ targetDir,
282
+ "src",
283
+ "main",
284
+ "java",
285
+ "com",
286
+ "boltic",
287
+ "io",
288
+ "serverless",
289
+ "Handler.java"
290
+ );
291
+ } else if (language === "golang") {
292
+ handlerFile = path.join(targetDir, `${handlerFileName}.go`);
293
+ } else if (language === "python") {
294
+ handlerFile = path.join(targetDir, `${handlerFileName}.py`);
295
+ } else {
296
+ handlerFile = path.join(targetDir, `${handlerFileName}.js`);
297
+ }
298
+
299
+ const code = fs.readFileSync(handlerFile, "utf-8");
300
+
301
+ // Build the payload for create API
302
+ const payload = {
303
+ Name: name,
304
+ Runtime: "code",
305
+ Env: {},
306
+ PortMap: [],
307
+ Scaling: {
308
+ AutoStop: false,
309
+ Min: 1,
310
+ Max: 1,
311
+ MaxIdleTime: 0,
312
+ },
313
+ Resources: {
314
+ CPU: 0.1,
315
+ MemoryMB: 128,
316
+ MemoryMaxMB: 128,
317
+ },
318
+ CodeOpts: {
319
+ Language: `${language}/${version}`,
320
+ Packages: [],
321
+ Code: code,
322
+ },
323
+ };
324
+
325
+ // Call create serverless API
326
+ console.log(chalk.cyan("\nšŸ“¤ Creating serverless function..."));
327
+ const response = await publishServerless(apiUrl, token, session, payload);
328
+
329
+ if (!response) {
330
+ console.error(chalk.red("\nāŒ Failed to create serverless function"));
331
+ return;
332
+ }
333
+
334
+ // Update boltic-properties.yaml with serverlessId
335
+ const serverlessId = response.ID || response.data?.ID || response._id;
336
+ if (serverlessId) {
337
+ const bolticYamlPath = path.join(targetDir, "boltic-properties.yaml");
338
+ let bolticYamlContent = fs.readFileSync(bolticYamlPath, "utf-8");
339
+ // Add serverlessId at the top after app line
340
+ bolticYamlContent = bolticYamlContent.replace(
341
+ /^(app: .*)$/m,
342
+ `$1\nserverlessId: "${serverlessId}"`
343
+ );
344
+ fs.writeFileSync(bolticYamlPath, bolticYamlContent);
345
+ }
346
+
347
+ // Display success message
348
+ console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
349
+ console.log(
350
+ chalk.green("šŸ“ Blueprint serverless function created successfully!")
351
+ );
352
+ console.log();
353
+ console.log(chalk.cyan(" Name: ") + chalk.white(name));
354
+ console.log(chalk.cyan(" Type: ") + chalk.white("code"));
355
+ console.log(
356
+ chalk.cyan(" Language: ") + chalk.white(`${language}/${version}`)
357
+ );
358
+ console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
359
+ if (serverlessId) {
360
+ console.log(
361
+ chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId)
362
+ );
363
+ }
364
+ console.log();
365
+
366
+ // Poll for serverless status until running
367
+ if (serverlessId) {
368
+ await pollServerlessStatus(pullServerless, serverlessId, {
369
+ apiUrl,
370
+ token,
371
+ accountId,
372
+ session,
373
+ });
374
+ }
375
+
376
+ console.log(chalk.yellow("šŸ“ Next steps:"));
377
+ console.log(chalk.dim(" 1. Edit your handler code"));
378
+ console.log(chalk.dim(" 2. Test locally: boltic serverless test"));
379
+ console.log(chalk.dim(" 3. Update: boltic serverless publish"));
380
+ console.log();
381
+ }
382
+
383
+ /**
384
+ * Handle git type serverless creation - creates folder with boltic-properties.yaml and calls create API
385
+ */
386
+ async function handleGitTypeCreate(name, language, version, targetDir) {
387
+ console.log(chalk.cyan("\nšŸ“ Creating git-based serverless project..."));
388
+ console.log(chalk.dim(` Type: git`));
389
+ console.log(chalk.dim(` Language: ${language}/${version}`));
390
+
391
+ // Get authentication credentials first
392
+ const env = await getCurrentEnv();
393
+ if (!env || !env.token || !env.session) {
394
+ console.error(chalk.red("\nāŒ Not authenticated. Please login first."));
395
+ console.log(chalk.yellow(" Run: boltic login"));
396
+ // Cleanup the created directory
397
+ try {
398
+ fs.rmSync(targetDir, { recursive: true, force: true });
399
+ } catch {
400
+ // Ignore cleanup errors
401
+ }
402
+ return;
403
+ }
404
+
405
+ const { apiUrl, token, session } = env;
406
+
407
+ // Build the payload for git type
408
+ const payload = {
409
+ Name: name,
410
+ Runtime: "git",
411
+ Env: {},
412
+ PortMap: [],
413
+ Scaling: {
414
+ AutoStop: false,
415
+ Min: 1,
416
+ Max: 1,
417
+ MaxIdleTime: 0,
418
+ },
419
+ Resources: {
420
+ CPU: 0.1,
421
+ MemoryMB: 128,
422
+ MemoryMaxMB: 128,
423
+ },
424
+ CodeOpts: {
425
+ Language: `${language}/${version}`,
426
+ },
427
+ };
428
+
429
+ // Call create serverless API
430
+ console.log(chalk.cyan("\nšŸ“¤ Creating git-based serverless function..."));
431
+ const response = await publishServerless(apiUrl, token, session, payload);
432
+
433
+ if (!response) {
434
+ console.error(chalk.red("\nāŒ Failed to create serverless function"));
435
+ // Cleanup the created directory
436
+ try {
437
+ fs.rmSync(targetDir, { recursive: true, force: true });
438
+ } catch {
439
+ // Ignore cleanup errors
440
+ }
441
+ return;
442
+ }
443
+
444
+ // Extract serverless ID and git info from response
445
+ // Response structure: { ID, Links: { Git: { Repository: { SshURL, HtmlURL, CloneURL, ... } } } }
446
+ const serverlessId = response.ID || response.data?.ID || response._id;
447
+ const gitRepo =
448
+ response.Links?.Git?.Repository ||
449
+ response.data?.Links?.Git?.Repository;
450
+ const gitSshUrl = gitRepo?.SshURL || "";
451
+ const gitHttpUrl = gitRepo?.HtmlURL || "";
452
+ const gitCloneUrl = gitRepo?.CloneURL || "";
453
+
454
+ // Create boltic-properties.yaml with serverlessId
455
+ const bolticYamlContent = `app: "${name}"
456
+ serverlessId: "${serverlessId}"
457
+ region: "asia-south1"
458
+ handler: "${HANDLER_MAPPING[language]}"
459
+ language: "${language}/${version}"
460
+
461
+ serverlessConfig:
462
+ Name: "${name}"
463
+ Description: ""
464
+ Runtime: "git"
465
+ # Environment variables for your serverless function
466
+ # To add env variables, replace {} with key-value pairs like:
467
+ # Env:
468
+ # API_KEY: "your-api-key"
469
+ #TO add port map, replace {} with port map like:
470
+ # PortMap:
471
+ # - Name: "port"
472
+ # Port: "8080"
473
+ # Protocol: "http"/"https"
474
+ Env: {}
475
+ PortMap: {}
476
+ Scaling:
477
+ AutoStop: false
478
+ Min: 1
479
+ Max: 1
480
+ MaxIdleTime: 300
481
+ Resources:
482
+ CPU: 0.1
483
+ MemoryMB: 128
484
+ MemoryMaxMB: 128
485
+ Timeout: 60
486
+ Validations: null
487
+
488
+ build:
489
+ builtin: dockerfile
490
+ ignorefile: .gitignore
491
+ `;
492
+
493
+ try {
494
+ fs.writeFileSync(
495
+ path.join(targetDir, "boltic-properties.yaml"),
496
+ bolticYamlContent
497
+ );
498
+ } catch (err) {
499
+ console.error(
500
+ chalk.red(`\nāŒ Failed to create boltic-properties.yaml`)
501
+ );
502
+ console.error(chalk.red(`Error: ${err.message}`));
503
+ return;
504
+ }
505
+
506
+ // Check if user has git access by trying ls-remote
507
+ let hasGitAccess = false;
508
+ if (gitSshUrl) {
509
+ console.log(chalk.cyan("\nšŸ” Checking git repository access..."));
510
+ try {
511
+ // Initialize git repo
512
+ execSync(`git init`, { cwd: targetDir, stdio: "pipe" });
513
+ execSync(`git remote add origin ${gitSshUrl}`, {
514
+ cwd: targetDir,
515
+ stdio: "pipe",
516
+ });
517
+ // Try ls-remote to check SSH access
518
+ execSync(`git ls-remote ${gitSshUrl}`, {
519
+ cwd: targetDir,
520
+ stdio: "pipe",
521
+ timeout: 15000,
522
+ });
523
+ hasGitAccess = true;
524
+ } catch (err) {
525
+ hasGitAccess = false;
526
+ }
527
+ }
528
+
529
+ // If user has access, create main branch
530
+ if (hasGitAccess) {
531
+ try {
532
+ console.log(chalk.cyan("šŸ”§ Setting up git branch..."));
533
+ // Create main branch
534
+ execSync(`git checkout -b main`, { cwd: targetDir, stdio: "pipe" });
535
+ console.log(chalk.green("āœ“ Created main branch"));
536
+ } catch (err) {
537
+ // Ignore errors in branch setup, user can do it manually
538
+ console.log(
539
+ chalk.yellow(
540
+ "āš ļø Could not auto-setup git branch. You can set it up manually."
541
+ )
542
+ );
543
+ }
544
+ }
545
+
546
+ // Display success message
547
+ console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
548
+ console.log(
549
+ chalk.green("šŸ“ Git-based serverless project created successfully!")
550
+ );
551
+ console.log();
552
+ console.log(chalk.cyan(" Name: ") + chalk.white(name));
553
+ console.log(chalk.cyan(" Type: ") + chalk.white("git"));
554
+ console.log(
555
+ chalk.cyan(" Language: ") + chalk.white(`${language}/${version}`)
556
+ );
557
+ console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
558
+ console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
559
+
560
+ if (gitSshUrl || gitHttpUrl) {
561
+ console.log();
562
+ console.log(chalk.cyan(" šŸ“¦ Git Repository:"));
563
+ if (gitSshUrl) {
564
+ console.log(chalk.cyan(" SSH URL: ") + chalk.white(gitSshUrl));
565
+ }
566
+ if (gitHttpUrl) {
567
+ console.log(
568
+ chalk.cyan(" Web URL: ") + chalk.white(gitHttpUrl)
569
+ );
570
+ }
571
+ if (gitCloneUrl) {
572
+ console.log(
573
+ chalk.cyan(" Clone URL: ") + chalk.white(gitCloneUrl)
574
+ );
575
+ }
576
+ console.log();
577
+
578
+ if (hasGitAccess) {
579
+ console.log(
580
+ chalk.green("āœ… You have access to the git repository!")
581
+ );
582
+ console.log(chalk.green("āœ… Main branch created!"));
583
+ console.log();
584
+ console.log(
585
+ chalk.yellow("šŸ“ Next steps - Add your code and push:")
586
+ );
587
+ console.log(chalk.dim(" 1. Add your server code to this folder"));
588
+ console.log(chalk.dim(" 2. Commit and push:"));
589
+ console.log(chalk.white(` git add .`));
590
+ console.log(chalk.white(` git commit -m "Initial commit"`));
591
+ console.log(chalk.white(` git push -u origin main`));
592
+ } else {
593
+ console.log(
594
+ chalk.red("āŒ You don't have access to this git repository.")
595
+ );
596
+ console.log(
597
+ chalk.yellow(
598
+ " Please add your SSH key from the Boltic UI to get access."
599
+ )
600
+ );
601
+ console.log();
602
+ console.log(
603
+ chalk.yellow("šŸ“ Once you have access, push your code:")
604
+ );
605
+ console.log(chalk.dim(" 1. Add your code to this folder"));
606
+ console.log(chalk.dim(" 2. Run:"));
607
+ console.log(chalk.white(` git checkout -b main`));
608
+ console.log(chalk.white(` git add .`));
609
+ console.log(chalk.white(` git commit -m "Initial commit"`));
610
+ console.log(chalk.white(` git push -u origin main`));
611
+ }
612
+ } else {
613
+ console.log();
614
+ console.log(chalk.yellow("šŸ“ Next steps:"));
615
+ console.log(chalk.dim(" 1. Add your code to this folder"));
616
+ console.log(chalk.dim(" 2. Configure git remote and push your code"));
617
+ }
618
+ console.log();
619
+ }
620
+
621
+ /**
622
+ * Handle container type serverless creation - creates empty folder with boltic-properties.yaml
623
+ */
624
+ async function handleContainerTypeCreate(name, targetDir) {
625
+ console.log(
626
+ chalk.cyan("\n🐳 Creating container-based serverless project...")
627
+ );
628
+ console.log(chalk.dim(` Type: container`));
629
+
630
+ // Ask for container image URI
631
+ const containerImage = await input({
632
+ message: "Enter container image URI (e.g., docker.io/user/image:tag):",
633
+ validate: (value) => {
634
+ if (!value || value.trim() === "") {
635
+ return "Container image URI is required";
636
+ }
637
+ return true;
638
+ },
639
+ });
640
+
641
+ console.log(chalk.cyan("\nšŸ“¤ Creating serverless function..."));
642
+
643
+ // Get auth credentials
644
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
645
+
646
+ // Build create payload for container type
647
+ const createPayload = {
648
+ Name: name,
649
+ Description: "",
650
+ Runtime: "container",
651
+ PortMap: [],
652
+ Scaling: {
653
+ AutoStop: false,
654
+ Min: 1,
655
+ Max: 1,
656
+ MaxIdleTime: 300,
657
+ },
658
+ Resources: {
659
+ CPU: 0.1,
660
+ MemoryMB: 128,
661
+ MemoryMaxMB: 128,
662
+ },
663
+ Timeout: 60,
664
+ Validations: null,
665
+ ContainerOpts: {
666
+ Image: containerImage.trim(),
667
+ Args: [],
668
+ Command: "",
669
+ },
670
+ };
671
+
672
+ // Call create serverless API
673
+ const response = await publishServerless(
674
+ apiUrl,
675
+ token,
676
+ session,
677
+ createPayload
678
+ );
679
+
680
+ if (!response || !response.ID) {
681
+ console.error(chalk.red("\nāŒ Failed to create serverless function"));
682
+ // Cleanup directory
683
+ try {
684
+ fs.rmSync(targetDir, { recursive: true, force: true });
685
+ } catch {
686
+ // Ignore cleanup errors
687
+ }
688
+ return;
689
+ }
690
+
691
+ const serverlessId = response.ID;
692
+
693
+ // Create boltic-properties.yaml for container type
694
+ const bolticYamlContent = `app: "${name}"
695
+ region: "asia-south1"
696
+ serverlessId: "${serverlessId}"
697
+
698
+ serverlessConfig:
699
+ Name: "${name}"
700
+ Description: ""
701
+ Runtime: "container"
702
+ # Environment variables for your serverless function
703
+ # To add env variables, replace {} with key-value pairs like:
704
+ # Env:
705
+ # API_KEY: "your-api-key"
706
+ Env: {}
707
+ PortMap: []
708
+ Scaling:
709
+ AutoStop: false
710
+ Min: 1
711
+ Max: 1
712
+ MaxIdleTime: 300
713
+ Resources:
714
+ CPU: 0.1
715
+ MemoryMB: 128
716
+ MemoryMaxMB: 128
717
+ Timeout: 60
718
+ Validations: null
719
+ ContainerOpts:
720
+ Image: "${containerImage.trim()}"
721
+ Args: []
722
+ Command: ""
723
+
724
+ build:
725
+ builtin: dockerfile
726
+ ignorefile: .gitignore
727
+ `;
728
+
729
+ try {
730
+ fs.writeFileSync(
731
+ path.join(targetDir, "boltic-properties.yaml"),
732
+ bolticYamlContent
733
+ );
734
+ } catch (err) {
735
+ console.error(
736
+ chalk.red(`\nāŒ Failed to create boltic-properties.yaml`)
737
+ );
738
+ console.error(chalk.red(`Error: ${err.message}`));
739
+ return;
740
+ }
741
+
742
+ // Display success message for container type
743
+ console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
744
+ console.log(
745
+ chalk.green(
746
+ "🐳 Container-based serverless project created successfully!"
747
+ )
748
+ );
749
+ console.log();
750
+ console.log(chalk.cyan(" Name: ") + chalk.white(name));
751
+ console.log(chalk.cyan(" Type: ") + chalk.white("container"));
752
+ console.log(chalk.cyan(" Image: ") + chalk.white(containerImage.trim()));
753
+ console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
754
+ console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
755
+ console.log();
756
+
757
+ // Poll for serverless status until running
758
+ await pollServerlessStatus(pullServerless, serverlessId, {
759
+ apiUrl,
760
+ token,
761
+ accountId,
762
+ session,
763
+ });
764
+
765
+ console.log(chalk.yellow("šŸ“ Next steps:"));
766
+ console.log(
767
+ chalk.dim(" 1. To update configuration, edit boltic-properties.yaml")
768
+ );
769
+ console.log(
770
+ chalk.dim(" 2. To publish changes: boltic serverless publish")
771
+ );
772
+ console.log();
773
+ }
774
+
775
+ /**
776
+ * Handle the publish serverless command
777
+ */
778
+ async function handlePublish(args = []) {
779
+ try {
780
+ console.log(
781
+ "\n" +
782
+ chalk.bgMagenta.black(" šŸš€ SERVERLESS PUBLISH ") +
783
+ chalk.magenta(" Deploy your serverless function\n")
784
+ );
785
+
786
+ // Step 1: Parse CLI arguments
787
+ const parsedArgs = parsePublishArgs(args);
788
+ const { directory } = parsedArgs;
789
+
790
+ // Validate directory exists
791
+ if (!fs.existsSync(directory)) {
792
+ console.error(
793
+ chalk.red(`\nāŒ Directory does not exist: ${directory}`)
794
+ );
795
+ return;
796
+ }
797
+
798
+ // Step 2: Load boltic-properties.yaml config
799
+ const config = loadBolticConfig(directory);
800
+ if (!config) {
801
+ console.error(
802
+ chalk.red(
803
+ "\nāŒ boltic-properties.yaml not found in the directory"
804
+ )
805
+ );
806
+ console.log(
807
+ chalk.yellow(
808
+ "Please run this command from a serverless project directory."
809
+ )
810
+ );
811
+ return;
812
+ }
813
+
814
+ // Step 3: Get app name and language from config
815
+ const appName = config.app;
816
+ const language = config.language; // e.g., "nodejs/20"
817
+ const serverlessId = config.serverlessId;
818
+ const serverlessConfig = config.serverlessConfig;
819
+
820
+ if (!appName) {
821
+ console.error(
822
+ chalk.red("\nāŒ App name not found in boltic-properties.yaml")
823
+ );
824
+ return;
825
+ }
826
+
827
+ if (!language && serverlessConfig?.Runtime !== "container") {
828
+ console.error(
829
+ chalk.red("\nāŒ Language not found in boltic-properties.yaml")
830
+ );
831
+ return;
832
+ }
833
+
834
+ console.log(chalk.cyan("šŸ“‹ App Name: ") + chalk.white(appName));
835
+ console.log(chalk.cyan("šŸ“‹ Language: ") + chalk.white(language));
836
+ console.log(
837
+ chalk.cyan("šŸ“‹ Runtime: ") +
838
+ chalk.white(serverlessConfig?.Runtime || "code")
839
+ );
840
+
841
+ // Step 4: Read handler file (only for "code" runtime type)
842
+ const languageBase = parseLanguageFromConfig(language);
843
+ const runtime = serverlessConfig?.Runtime || "code";
844
+ let code = null;
845
+
846
+ if (runtime === "code") {
847
+ code = readHandlerFile(directory, languageBase, config);
848
+
849
+ if (!code) {
850
+ console.error(chalk.red("\nāŒ Handler file not found"));
851
+ const handlerConfig = parseHandlerConfig(
852
+ config.handler,
853
+ languageBase
854
+ );
855
+ console.log(
856
+ chalk.yellow(`Expected handler file: ${handlerConfig.file}`)
857
+ );
858
+ return;
859
+ }
860
+
861
+ console.log(chalk.cyan("šŸ“„ Handler code loaded successfully"));
862
+ }
863
+
864
+ // Step 5: Get auth credentials
865
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
866
+
867
+ let response;
868
+
869
+ // Update existing serverless function
870
+ const payload = buildUpdatePayload(serverlessConfig, language, code);
871
+
872
+ console.log(chalk.cyan("\nšŸ“¤ Updating serverless function..."));
873
+ response = await updateServerless(
874
+ apiUrl,
875
+ token,
876
+ session,
877
+ serverlessId,
878
+ payload
879
+ );
880
+
881
+ if (response) {
882
+ displayPublishSuccessMessage(appName, response);
883
+
884
+ // Poll for serverless status for code and container types only
885
+ if (runtime === "code" || runtime === "container") {
886
+ await pollServerlessStatus(pullServerless, serverlessId, {
887
+ apiUrl,
888
+ token,
889
+ accountId,
890
+ session,
891
+ });
892
+ }
893
+ } else {
894
+ console.error(
895
+ chalk.red(`\nāŒ Failed to publish serverless function`)
896
+ );
897
+ }
898
+ } catch (error) {
899
+ if (
900
+ error.message &&
901
+ error.message.includes("User force closed the prompt")
902
+ ) {
903
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
904
+ return;
905
+ }
906
+ console.error(
907
+ chalk.red("\nāŒ An error occurred:"),
908
+ error.message || "Unknown error"
909
+ );
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Handle the test serverless command
915
+ */
916
+ async function handleTest(args = []) {
917
+ let childProcess = null;
918
+ let language = null;
919
+ let directory = null;
920
+ let retain = false;
921
+
922
+ // Setup cleanup handler
923
+ const cleanup = (signal) => {
924
+ console.log(chalk.yellow(`\n\nāš ļø ${signal} received, cleaning up...`));
925
+
926
+ if (childProcess) {
927
+ childProcess.kill("SIGTERM");
928
+ }
929
+
930
+ if (language && directory) {
931
+ cleanupGeneratedFiles(directory, language, retain);
932
+ }
933
+
934
+ process.exit(0);
935
+ };
936
+
937
+ // Register signal handlers
938
+ process.on("SIGINT", () => cleanup("SIGINT"));
939
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
940
+
941
+ try {
942
+ // Step 1: Parse CLI arguments
943
+ const parsedArgs = parseTestArgs(args);
944
+ let {
945
+ port,
946
+ handlerFile,
947
+ handlerFunction,
948
+ command: customCommand,
949
+ } = parsedArgs;
950
+ language = parsedArgs.language;
951
+ directory = parsedArgs.directory;
952
+ retain = parsedArgs.retain;
953
+
954
+ // Validate directory exists
955
+ if (!fs.existsSync(directory)) {
956
+ console.error(
957
+ chalk.red(`\nāŒ Directory does not exist: ${directory}`)
958
+ );
959
+ return;
960
+ }
961
+
962
+ // Step 2: Load boltic-properties.yaml config
963
+ const config = loadBolticConfig(directory);
964
+ if (!config) {
965
+ console.error(
966
+ chalk.red(
967
+ "\nāŒ boltic-properties.yaml not found in the directory"
968
+ )
969
+ );
970
+ console.log(
971
+ chalk.yellow(
972
+ "You can only test code or container type serverless with boltic-properties.yaml"
973
+ )
974
+ );
975
+ return;
976
+ }
977
+
978
+ // Check if it's a container type serverless
979
+ const runtime = config.serverlessConfig?.Runtime || "code";
980
+ if (runtime === "container") {
981
+ await handleContainerTest(config, directory, port);
982
+ return;
983
+ }
984
+
985
+ // For git type, show message that test is not supported
986
+ if (runtime === "git") {
987
+ console.log(
988
+ chalk.yellow(
989
+ "\nāš ļø Git type serverless test is not supported via CLI."
990
+ )
991
+ );
992
+ console.log(
993
+ chalk.dim(
994
+ "For git type, run your server directly using your project's start command."
995
+ )
996
+ );
997
+ console.log(
998
+ chalk.dim("Example: npm start, python app.py, go run ., etc.")
999
+ );
1000
+ return;
1001
+ }
1002
+
1003
+ // Step 3: Determine language (for code type)
1004
+ if (!language && config?.language) {
1005
+ language = parseLanguageFromConfig(config.language);
1006
+ console.log(
1007
+ chalk.cyan("šŸ“‹ Using language from boltic-properties.yaml: ") +
1008
+ chalk.bold.white(language)
1009
+ );
1010
+ }
1011
+
1012
+ if (!language) {
1013
+ console.log(
1014
+ chalk.yellow("āš ļø No language specified, auto-detecting...")
1015
+ );
1016
+ language = detectLanguage(directory);
1017
+ }
1018
+
1019
+ if (!language) {
1020
+ console.error(
1021
+ chalk.red(
1022
+ "\nāŒ Could not detect language. Please specify with --language flag."
1023
+ )
1024
+ );
1025
+ console.log(
1026
+ chalk.yellow(
1027
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
1028
+ )
1029
+ );
1030
+ return;
1031
+ }
1032
+
1033
+ // Validate language
1034
+ if (!SUPPORTED_LANGUAGES.includes(language)) {
1035
+ console.error(chalk.red(`\nāŒ Unsupported language: ${language}`));
1036
+ console.log(
1037
+ chalk.yellow(
1038
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
1039
+ )
1040
+ );
1041
+ return;
1042
+ }
1043
+
1044
+ // Step 4: Determine handler file and function
1045
+ if (!handlerFile || !handlerFunction) {
1046
+ const handlerConfig = parseHandlerConfig(config?.handler, language);
1047
+ handlerFile = handlerFile || handlerConfig.file;
1048
+ handlerFunction = handlerFunction || handlerConfig.function;
1049
+ }
1050
+
1051
+ // Verify handler file exists
1052
+ const handlerPath = path.join(directory, handlerFile);
1053
+ if (!fs.existsSync(handlerPath)) {
1054
+ console.error(
1055
+ chalk.red(`\nāŒ Handler file not found: ${handlerPath}`)
1056
+ );
1057
+ console.log(
1058
+ chalk.yellow(
1059
+ "Please specify the correct handler file with --handler-file flag."
1060
+ )
1061
+ );
1062
+ return;
1063
+ }
1064
+
1065
+ // Step 4.1: Detect actual handler function name from code
1066
+ // This handles cases where user might have renamed the function (e.g., handler -> handler1)
1067
+ const handlerCode = fs.readFileSync(handlerPath, "utf8");
1068
+ const detectedFunction = detectHandlerFunctionFromCode(
1069
+ handlerCode,
1070
+ language
1071
+ );
1072
+
1073
+ if (detectedFunction && detectedFunction !== handlerFunction) {
1074
+ console.log(
1075
+ chalk.yellow(`āš ļø Detected handler function: `) +
1076
+ chalk.bold.white(detectedFunction) +
1077
+ chalk.yellow(` (config says: ${handlerFunction})`)
1078
+ );
1079
+ console.log(
1080
+ chalk.cyan(" Using detected function name from code...")
1081
+ );
1082
+ handlerFunction = detectedFunction;
1083
+ }
1084
+
1085
+ console.log(
1086
+ chalk.cyan("šŸ“¦ Handler: ") +
1087
+ chalk.white(`${handlerFile}.${handlerFunction}`)
1088
+ );
1089
+
1090
+ // Step 5: Install dependencies
1091
+ if (language === "nodejs") {
1092
+ const missingDeps = checkNodeDependencies(
1093
+ directory,
1094
+ REQUIRED_DEPENDENCIES.nodejs
1095
+ );
1096
+
1097
+ if (missingDeps.length > 0) {
1098
+ console.log(
1099
+ chalk.yellow(
1100
+ `\nšŸ“¦ Missing dependencies: ${missingDeps.join(", ")}`
1101
+ )
1102
+ );
1103
+ console.log(chalk.cyan(" Installing with --no-save..."));
1104
+
1105
+ try {
1106
+ execSync(`npm install ${missingDeps.join(" ")} --no-save`, {
1107
+ cwd: directory,
1108
+ stdio: "inherit",
1109
+ });
1110
+ console.log(chalk.green(" āœ“ Dependencies installed"));
1111
+ } catch (error) {
1112
+ console.error(
1113
+ chalk.red("\nāŒ Failed to install dependencies")
1114
+ );
1115
+ console.error(chalk.red(`Error: ${error.message}`));
1116
+ return;
1117
+ }
1118
+ }
1119
+ }
1120
+
1121
+ // Install Python dependencies using virtual environment
1122
+ if (language === "python") {
1123
+ const venvPath = path.join(directory, ".venv");
1124
+ const venvPython = path.join(venvPath, "bin", "python3");
1125
+ const venvPip = path.join(venvPath, "bin", "pip3");
1126
+
1127
+ // Create virtual environment if it doesn't exist
1128
+ if (!fs.existsSync(venvPath)) {
1129
+ console.log(
1130
+ chalk.cyan("\nšŸ“¦ Creating Python virtual environment...")
1131
+ );
1132
+ try {
1133
+ execSync(`python3 -m venv .venv`, {
1134
+ cwd: directory,
1135
+ stdio: "inherit",
1136
+ });
1137
+ console.log(
1138
+ chalk.green(" āœ“ Virtual environment created")
1139
+ );
1140
+ } catch (error) {
1141
+ console.error(
1142
+ chalk.red("\nāŒ Failed to create virtual environment")
1143
+ );
1144
+ console.error(chalk.red(`Error: ${error.message}`));
1145
+ return;
1146
+ }
1147
+ }
1148
+
1149
+ // Install dependencies in the virtual environment
1150
+ const depsToInstall = REQUIRED_DEPENDENCIES.python;
1151
+ console.log(
1152
+ chalk.cyan(
1153
+ `\nšŸ“¦ Installing Python packages: ${depsToInstall.join(", ")}`
1154
+ )
1155
+ );
1156
+
1157
+ try {
1158
+ execSync(`${venvPip} install ${depsToInstall.join(" ")}`, {
1159
+ cwd: directory,
1160
+ stdio: "inherit",
1161
+ });
1162
+ console.log(chalk.green(" āœ“ Python packages installed"));
1163
+ } catch (error) {
1164
+ console.error(
1165
+ chalk.red("\nāŒ Failed to install Python packages")
1166
+ );
1167
+ console.error(chalk.red(`Error: ${error.message}`));
1168
+ return;
1169
+ }
1170
+ }
1171
+
1172
+ // Step 6: Generate test files (wrapper + additional files like pom.xml for Java)
1173
+ console.log(chalk.cyan("\nšŸ“ Generating test files..."));
1174
+
1175
+ // Get app name from config or directory name
1176
+ const appName = config?.app || path.basename(directory);
1177
+
1178
+ const testFiles = generateTestFiles(
1179
+ language,
1180
+ handlerFile,
1181
+ handlerFunction,
1182
+ appName
1183
+ );
1184
+
1185
+ if (!testFiles || testFiles.length === 0) {
1186
+ console.error(
1187
+ chalk.red(
1188
+ `\nāŒ Failed to generate test files for language: ${language}`
1189
+ )
1190
+ );
1191
+ return;
1192
+ }
1193
+
1194
+ // Write all generated files
1195
+ for (const file of testFiles) {
1196
+ const filePath = path.join(directory, file.path);
1197
+
1198
+ // Create directories if needed
1199
+ const fileDir = path.dirname(filePath);
1200
+ if (!fs.existsSync(fileDir)) {
1201
+ fs.mkdirSync(fileDir, { recursive: true });
1202
+ }
1203
+
1204
+ fs.writeFileSync(filePath, file.content, "utf8");
1205
+ console.log(chalk.dim(` Created: ${file.path}`));
1206
+ }
1207
+
1208
+ // Step 7: Determine start command
1209
+ const startCmd = getStartCommand(language, directory, customCommand);
1210
+
1211
+ // Step 8: Set environment variables
1212
+ const env = getTestEnvironmentVariables(port, language);
1213
+
1214
+ // Step 9: Display startup message
1215
+ displayTestStartupMessage(port);
1216
+
1217
+ // Step 10: Start the server
1218
+ childProcess = spawn(startCmd.command, startCmd.args, {
1219
+ cwd: directory,
1220
+ env,
1221
+ stdio: ["inherit", "pipe", "pipe"],
1222
+ shell: process.platform === "win32",
1223
+ });
1224
+
1225
+ // Stream stdout
1226
+ childProcess.stdout.on("data", (data) => {
1227
+ process.stdout.write(chalk.white(data.toString()));
1228
+ });
1229
+
1230
+ // Stream stderr
1231
+ childProcess.stderr.on("data", (data) => {
1232
+ process.stderr.write(chalk.red(data.toString()));
1233
+ });
1234
+
1235
+ // Handle process exit
1236
+ childProcess.on("close", (code) => {
1237
+ console.log(
1238
+ chalk.yellow(`\nšŸ›‘ Server stopped with exit code: ${code}`)
1239
+ );
1240
+ cleanupGeneratedFiles(directory, language, retain);
1241
+ process.exit(code || 0);
1242
+ });
1243
+
1244
+ // Handle process error
1245
+ childProcess.on("error", (error) => {
1246
+ console.error(
1247
+ chalk.red(`\nāŒ Failed to start server: ${error.message}`)
1248
+ );
1249
+
1250
+ if (error.code === "ENOENT") {
1251
+ console.log(
1252
+ chalk.yellow(
1253
+ `\nšŸ’” Hint: Make sure the command "${startCmd.command}" is installed and available in PATH.`
1254
+ )
1255
+ );
1256
+ }
1257
+
1258
+ cleanupGeneratedFiles(directory, language, retain);
1259
+ process.exit(1);
1260
+ });
1261
+ } catch (error) {
1262
+ if (
1263
+ error.message &&
1264
+ error.message.includes("User force closed the prompt")
1265
+ ) {
1266
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1267
+ if (language && directory) {
1268
+ cleanupGeneratedFiles(directory, language, retain);
1269
+ }
1270
+ return;
1271
+ }
1272
+
1273
+ console.error(
1274
+ chalk.red("\nāŒ An error occurred:"),
1275
+ error.message || "Unknown error"
1276
+ );
1277
+
1278
+ if (language && directory) {
1279
+ cleanupGeneratedFiles(directory, language, retain);
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Handle container type serverless test - runs docker container locally
1286
+ */
1287
+ async function handleContainerTest(config, directory, port) {
1288
+ const containerOpts = config.serverlessConfig?.ContainerOpts;
1289
+ const image = containerOpts?.Image;
1290
+
1291
+ if (!image) {
1292
+ console.error(
1293
+ chalk.red(
1294
+ "\nāŒ Container image not found in boltic-properties.yaml"
1295
+ )
1296
+ );
1297
+ console.log(
1298
+ chalk.yellow(
1299
+ "Please ensure ContainerOpts.Image is set in serverlessConfig."
1300
+ )
1301
+ );
1302
+ return;
1303
+ }
1304
+
1305
+ console.log(chalk.cyan("\n🐳 Container serverless detected"));
1306
+ console.log(chalk.dim(` Image: ${image}`));
1307
+ console.log(chalk.dim(` Port: ${port}`));
1308
+
1309
+ // Check if Docker is available
1310
+ try {
1311
+ execSync("docker --version", { stdio: "pipe" });
1312
+ } catch (err) {
1313
+ console.error(
1314
+ chalk.red("\nāŒ Docker is not installed or not available in PATH.")
1315
+ );
1316
+ console.log(
1317
+ chalk.yellow(
1318
+ "Please install Docker to test container type serverless."
1319
+ )
1320
+ );
1321
+ return;
1322
+ }
1323
+
1324
+ // Build environment variables from config
1325
+ const envVars = config.serverlessConfig?.Env || {};
1326
+ const envArgs = Object.entries(envVars).flatMap(([key, value]) => [
1327
+ "-e",
1328
+ `${key}=${value}`,
1329
+ ]);
1330
+
1331
+ // Build docker run command
1332
+ const dockerArgs = ["run", "--rm", "-p", `${port}:8080`, ...envArgs, image];
1333
+
1334
+ console.log("\n" + chalk.bgCyan.black(" 🧪 LOCAL CONTAINER TEST ") + "\n");
1335
+ console.log(
1336
+ chalk.green(`šŸš€ Starting container on http://localhost:${port}`)
1337
+ );
1338
+ console.log();
1339
+ console.log(chalk.dim("━".repeat(60)));
1340
+ console.log(chalk.dim(" Press Ctrl+C to stop the container"));
1341
+ console.log(chalk.dim("━".repeat(60)));
1342
+ console.log();
1343
+
1344
+ // Start the container
1345
+ const dockerProcess = spawn("docker", dockerArgs, {
1346
+ cwd: directory,
1347
+ stdio: ["inherit", "pipe", "pipe"],
1348
+ });
1349
+
1350
+ // Stream stdout
1351
+ dockerProcess.stdout.on("data", (data) => {
1352
+ process.stdout.write(chalk.white(data.toString()));
1353
+ });
1354
+
1355
+ // Stream stderr
1356
+ dockerProcess.stderr.on("data", (data) => {
1357
+ process.stderr.write(chalk.yellow(data.toString()));
1358
+ });
1359
+
1360
+ // Handle process exit
1361
+ dockerProcess.on("close", (code) => {
1362
+ console.log(
1363
+ chalk.yellow(`\nšŸ›‘ Container stopped with exit code: ${code}`)
1364
+ );
1365
+ process.exit(code || 0);
1366
+ });
1367
+
1368
+ // Handle process error
1369
+ dockerProcess.on("error", (error) => {
1370
+ console.error(
1371
+ chalk.red(`\nāŒ Failed to start container: ${error.message}`)
1372
+ );
1373
+ if (error.code === "ENOENT") {
1374
+ console.log(
1375
+ chalk.yellow(
1376
+ "\nšŸ’” Hint: Make sure Docker is installed and available in PATH."
1377
+ )
1378
+ );
1379
+ }
1380
+ process.exit(1);
1381
+ });
1382
+
1383
+ // Handle Ctrl+C
1384
+ const cleanup = (signal) => {
1385
+ console.log(
1386
+ chalk.yellow(`\n\nšŸ›‘ Received ${signal}, stopping container...`)
1387
+ );
1388
+ dockerProcess.kill("SIGTERM");
1389
+ };
1390
+
1391
+ process.on("SIGINT", () => cleanup("SIGINT"));
1392
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
1393
+ }
1394
+
1395
+ async function handlePull(args) {
1396
+ console.log(chalk.green("Pulling serverless..."));
1397
+ try {
1398
+ // Parse command line arguments
1399
+ let currentDir = process.cwd();
1400
+ const pathIndex = args.indexOf("--path");
1401
+
1402
+ if (pathIndex !== -1 && args[pathIndex + 1]) {
1403
+ currentDir = args[pathIndex + 1];
1404
+ // Validate the provided path
1405
+ if (!fs.existsSync(currentDir)) {
1406
+ console.error(
1407
+ chalk.red(
1408
+ `Error: The specified path does not exist: ${currentDir}`
1409
+ )
1410
+ );
1411
+ return;
1412
+ }
1413
+ }
1414
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1415
+
1416
+ console.log(
1417
+ chalk.green(
1418
+ "Please select the serverless to pull from the list below:"
1419
+ )
1420
+ );
1421
+
1422
+ const allServerless = await listAllServerless(
1423
+ apiUrl,
1424
+ token,
1425
+ accountId,
1426
+ session
1427
+ );
1428
+ if (!allServerless || !Array.isArray(allServerless)) {
1429
+ console.error(
1430
+ chalk.red(
1431
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1432
+ )
1433
+ );
1434
+ }
1435
+ if (allServerless.length === 0) {
1436
+ console.error(chalk.red("\nāŒ No serverless found."));
1437
+ return;
1438
+ }
1439
+ // Let user select an integration
1440
+ const choices =
1441
+ allServerless.map((serverless) => {
1442
+ const runtime = serverless.Config?.Runtime || "code";
1443
+ const typeIcon =
1444
+ runtime === "git"
1445
+ ? "šŸ“¦"
1446
+ : runtime === "container"
1447
+ ? "🐳"
1448
+ : "šŸ“";
1449
+ const language = serverless.Config?.CodeOpts?.Language;
1450
+ return {
1451
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${serverless.Status}${language ? ` | language: ${language}` : ""}`,
1452
+ value: serverless,
1453
+ };
1454
+ }) || [];
1455
+
1456
+ const selectedServerless = await search({
1457
+ message: "Search and select an serverless to edit:",
1458
+ source: async (term) => {
1459
+ if (!term) return choices;
1460
+ return choices?.filter((choice) =>
1461
+ choice.name.toLowerCase().includes(term.toLowerCase())
1462
+ );
1463
+ },
1464
+ });
1465
+
1466
+ console.log(
1467
+ chalk.cyan("\nSelected serverless:"),
1468
+ selectedServerless.Config.Name
1469
+ );
1470
+ const pulledServerless = await pullServerless(
1471
+ apiUrl,
1472
+ token,
1473
+ accountId,
1474
+ session,
1475
+ selectedServerless.ID
1476
+ );
1477
+ if (!pulledServerless) {
1478
+ console.error(
1479
+ chalk.red(
1480
+ "\nāŒ Failed to fetch serverless details. Please try again later."
1481
+ )
1482
+ );
1483
+ return;
1484
+ }
1485
+ // console.log("selectes serverless : ",pulledServerless)
1486
+
1487
+ // Get the app name, language and type for the folder name
1488
+ const appName =
1489
+ pulledServerless?.Config?.Name || selectedServerless.Config?.Name;
1490
+ const language =
1491
+ pulledServerless?.Config?.CodeOpts?.Language?.split("/")[0] ||
1492
+ "nodejs";
1493
+ const serverlessType = pulledServerless?.Config?.Runtime || "code";
1494
+
1495
+ // Create folder name similar to create command
1496
+ const folderName = appName;
1497
+ const targetDir = path.join(currentDir, folderName);
1498
+
1499
+ // Check if folder already exists
1500
+ if (fs.existsSync(targetDir)) {
1501
+ console.error(
1502
+ chalk.red(
1503
+ `\nāŒ Folder "${folderName}" already exists in ${currentDir}. Please remove it or use a different location.`
1504
+ )
1505
+ );
1506
+ return;
1507
+ }
1508
+
1509
+ // Create the folder
1510
+ fs.mkdirSync(targetDir, { recursive: true });
1511
+ console.log(chalk.cyan(`\nšŸ“ Creating folder: ${folderName}`));
1512
+
1513
+ // Create the files (boltic-properties.yaml with serverlessId and serverlessConfig, handler file with code)
1514
+ try {
1515
+ const result = createPulledServerlessFiles(
1516
+ targetDir,
1517
+ pulledServerless,
1518
+ serverlessType
1519
+ );
1520
+
1521
+ // If there was an error (e.g., no SSH access for git type), don't show success
1522
+ if (result?.error) {
1523
+ return;
1524
+ }
1525
+
1526
+ displayPullSuccessMessage(appName, targetDir);
1527
+ } catch (fileError) {
1528
+ console.error(
1529
+ chalk.red("\nāŒ Failed to create files:"),
1530
+ fileError.message
1531
+ );
1532
+ // Clean up the created folder on error
1533
+ try {
1534
+ fs.rmSync(targetDir, { recursive: true, force: true });
1535
+ } catch (cleanupError) {
1536
+ // Ignore cleanup errors
1537
+ }
1538
+ return;
1539
+ }
1540
+ } catch (error) {
1541
+ if (
1542
+ error.message &&
1543
+ error.message.includes("User force closed the prompt")
1544
+ ) {
1545
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1546
+ return;
1547
+ }
1548
+ // Handle other errors
1549
+ console.error(
1550
+ chalk.red("\nāŒ An error occurred:"),
1551
+ error.message || "Unknown error"
1552
+ );
1553
+ }
1554
+ }
1555
+
1556
+ function showHelp() {
1557
+ console.log(chalk.cyan("\nServerless Commands:\n"));
1558
+ Object.entries(commands).forEach(([cmd, details]) => {
1559
+ console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
1560
+ });
1561
+
1562
+ console.log(chalk.cyan("\nCreate Command Options:\n"));
1563
+ console.log(
1564
+ chalk.bold(" --type, -t") +
1565
+ chalk.dim(" ") +
1566
+ "Serverless type: blueprint, git, or container (prompts if not provided)"
1567
+ );
1568
+ console.log(
1569
+ chalk.bold(" --name, -n") +
1570
+ chalk.dim(" ") +
1571
+ "Name of the serverless function (required, prompts if not provided)"
1572
+ );
1573
+ console.log(
1574
+ chalk.bold(" --language, -l") +
1575
+ chalk.dim(" ") +
1576
+ "Programming language: nodejs, python, golang, java (prompts if not provided)"
1577
+ );
1578
+ console.log(
1579
+ chalk.bold(" --directory, -d") +
1580
+ chalk.dim(" ") +
1581
+ "Directory where to create the project (default: current directory)"
1582
+ );
1583
+
1584
+ console.log(chalk.cyan("\nTest Command Options:\n"));
1585
+ console.log(
1586
+ chalk.bold(" --port, -p") +
1587
+ chalk.dim(" ") +
1588
+ "Port to run the server on (default: 8080)"
1589
+ );
1590
+ console.log(
1591
+ chalk.bold(" --language, -l") +
1592
+ chalk.dim(" ") +
1593
+ "Language (nodejs, python, golang, java) - auto-detected if not specified"
1594
+ );
1595
+ console.log(
1596
+ chalk.bold(" --directory, -d") +
1597
+ chalk.dim(" ") +
1598
+ "Base directory of the project (default: current directory)"
1599
+ );
1600
+
1601
+ console.log(chalk.cyan("\nPublish Command Options:\n"));
1602
+ console.log(
1603
+ chalk.bold(" --directory, -d") +
1604
+ chalk.dim(" ") +
1605
+ "Directory of the serverless project (default: current directory)"
1606
+ );
1607
+
1608
+ console.log(chalk.cyan("\nStatus Command Options:\n"));
1609
+ console.log(
1610
+ chalk.bold(" --name, -n") +
1611
+ chalk.dim(" ") +
1612
+ "Name of the serverless function (prompts if not provided)"
1613
+ );
1614
+
1615
+ console.log(chalk.cyan("\nCreate Examples:\n"));
1616
+ console.log(
1617
+ chalk.dim(
1618
+ " # Interactive mode (will prompt for type, name, and language)"
1619
+ )
1620
+ );
1621
+ console.log(" boltic serverless create\n");
1622
+ console.log(chalk.dim(" # Create blueprint serverless"));
1623
+ console.log(
1624
+ " boltic serverless create --type blueprint --name my-api --language nodejs\n"
1625
+ );
1626
+ console.log(
1627
+ chalk.dim(
1628
+ " # Create git-based serverless (add your code, then publish)"
1629
+ )
1630
+ );
1631
+ console.log(
1632
+ " boltic serverless create --type git --name my-git-func --language python\n"
1633
+ );
1634
+ console.log(chalk.dim(" # Create container-based serverless"));
1635
+ console.log(
1636
+ " boltic serverless create --type container --name my-container --language golang\n"
1637
+ );
1638
+ console.log(chalk.dim(" # With custom directory"));
1639
+ console.log(
1640
+ " boltic serverless create --type blueprint --name my-function --language python --directory ./projects\n"
1641
+ );
1642
+
1643
+ console.log(chalk.cyan("\nTest Examples:\n"));
1644
+ console.log(chalk.dim(" # Basic usage - auto-detect everything"));
1645
+ console.log(" boltic serverless test\n");
1646
+ console.log(chalk.dim(" # Specify port"));
1647
+ console.log(" boltic serverless test --port 3000\n");
1648
+
1649
+ console.log(chalk.cyan("\nPublish Examples:\n"));
1650
+ console.log(chalk.dim(" # Publish from current directory"));
1651
+ console.log(" boltic serverless publish\n");
1652
+ console.log(chalk.dim(" # Publish from specific directory"));
1653
+ console.log(" boltic serverless publish -d ./my-function\n");
1654
+
1655
+ console.log(chalk.cyan("\nList Examples:\n"));
1656
+ console.log(chalk.dim(" # List all serverless functions"));
1657
+ console.log(" boltic serverless list\n");
1658
+
1659
+ console.log(chalk.cyan("\nStatus Examples:\n"));
1660
+ console.log(chalk.dim(" # Get status by name"));
1661
+ console.log(" boltic serverless status -n my-function\n");
1662
+ console.log(chalk.dim(" # Interactive mode (will prompt for name)"));
1663
+ console.log(" boltic serverless status\n");
1664
+ }
1665
+
1666
+ // Execute the serverless command
1667
+ const execute = async (args) => {
1668
+ const subCommand = args[0];
1669
+
1670
+ if (!subCommand) {
1671
+ showHelp();
1672
+ return;
1673
+ }
1674
+
1675
+ if (!commands[subCommand]) {
1676
+ console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
1677
+ showHelp();
1678
+ return;
1679
+ }
1680
+
1681
+ const commandObj = commands[subCommand];
1682
+ await commandObj.action(args.slice(1));
1683
+ };
1684
+
1685
+ async function handleList(args = []) {
1686
+ try {
1687
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1688
+
1689
+ console.log(chalk.cyan("\nšŸ“‹ Fetching serverless functions...\n"));
1690
+
1691
+ const allServerless = await listAllServerless(
1692
+ apiUrl,
1693
+ token,
1694
+ accountId,
1695
+ session
1696
+ );
1697
+
1698
+ if (!allServerless || !Array.isArray(allServerless)) {
1699
+ console.error(
1700
+ chalk.red(
1701
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1702
+ )
1703
+ );
1704
+ return;
1705
+ }
1706
+
1707
+ if (allServerless.length === 0) {
1708
+ console.log(chalk.yellow("No serverless functions found."));
1709
+ return;
1710
+ }
1711
+
1712
+ console.log(
1713
+ chalk.green(`Found ${allServerless.length} serverless function(s):`)
1714
+ );
1715
+ console.log(
1716
+ chalk.dim("Use ↑↓ to scroll, type to search, Ctrl+C to exit\n")
1717
+ );
1718
+
1719
+ // Build choices for the list
1720
+ const choices = allServerless.map((serverless) => {
1721
+ const runtime = serverless.Config?.Runtime || "code";
1722
+ const typeIcon =
1723
+ runtime === "git"
1724
+ ? "šŸ“¦"
1725
+ : runtime === "container"
1726
+ ? "🐳"
1727
+ : "šŸ“";
1728
+ const language = serverless.Config?.CodeOpts?.Language;
1729
+ const status = serverless.Status;
1730
+
1731
+ return {
1732
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${status}${language ? ` | ${language}` : ""} | ID: ${serverless.ID.substring(0, 8)}...`,
1733
+ value: serverless,
1734
+ };
1735
+ });
1736
+
1737
+ // Show interactive scrollable list
1738
+ const selected = await search({
1739
+ message: "Serverless functions (scroll to browse):",
1740
+ source: async (term) => {
1741
+ if (!term) return choices;
1742
+ return choices.filter((choice) =>
1743
+ choice.name.toLowerCase().includes(term.toLowerCase())
1744
+ );
1745
+ },
1746
+ });
1747
+
1748
+ // Show details of selected serverless
1749
+ if (selected) {
1750
+ const runtime = selected.Config?.Runtime || "code";
1751
+ const typeIcon =
1752
+ runtime === "git"
1753
+ ? "šŸ“¦"
1754
+ : runtime === "container"
1755
+ ? "🐳"
1756
+ : "šŸ“";
1757
+
1758
+ console.log("\n" + chalk.cyan("━".repeat(60)));
1759
+ console.log(chalk.bold("\nšŸ“Œ Selected Serverless Details:\n"));
1760
+ console.log(
1761
+ chalk.cyan(" Name: ") + chalk.white(selected.Config.Name)
1762
+ );
1763
+ console.log(chalk.cyan(" ID: ") + chalk.white(selected.ID));
1764
+ console.log(
1765
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1766
+ );
1767
+ console.log(
1768
+ chalk.cyan(" Status: ") + chalk.white(selected.Status)
1769
+ );
1770
+ if (selected.Config?.CodeOpts?.Language) {
1771
+ console.log(
1772
+ chalk.cyan(" Language: ") +
1773
+ chalk.white(selected.Config.CodeOpts.Language)
1774
+ );
1775
+ }
1776
+ if (selected.Config?.ContainerOpts?.Image) {
1777
+ console.log(
1778
+ chalk.cyan(" Image: ") +
1779
+ chalk.white(selected.Config.ContainerOpts.Image)
1780
+ );
1781
+ }
1782
+ console.log(chalk.cyan("━".repeat(60)));
1783
+ console.log(
1784
+ chalk.dim(
1785
+ "\nUse 'boltic serverless pull' to pull this serverless locally."
1786
+ )
1787
+ );
1788
+ }
1789
+ } catch (error) {
1790
+ if (
1791
+ error.message &&
1792
+ error.message.includes("User force closed the prompt")
1793
+ ) {
1794
+ console.log(chalk.yellow("\nāš ļø List closed"));
1795
+ return;
1796
+ }
1797
+ console.error(
1798
+ chalk.red("\nāŒ An error occurred:"),
1799
+ error.message || "Unknown error"
1800
+ );
1801
+ }
1802
+ }
1803
+
1804
+ /**
1805
+ * Handle the status command - show status of a serverless function
1806
+ */
1807
+ async function handleStatus(args = []) {
1808
+ try {
1809
+ // Parse name from args
1810
+ let name = null;
1811
+ const nameIndex = args.indexOf("--name");
1812
+ const shortNameIndex = args.indexOf("-n");
1813
+
1814
+ if (nameIndex !== -1 && args[nameIndex + 1]) {
1815
+ name = args[nameIndex + 1];
1816
+ } else if (shortNameIndex !== -1 && args[shortNameIndex + 1]) {
1817
+ name = args[shortNameIndex + 1];
1818
+ }
1819
+
1820
+ // If name not provided, prompt for it
1821
+ if (!name) {
1822
+ name = await input({
1823
+ message: "Enter serverless name:",
1824
+ validate: (value) => {
1825
+ if (!value || value.trim() === "") {
1826
+ return "Serverless name is required";
1827
+ }
1828
+ return true;
1829
+ },
1830
+ });
1831
+ }
1832
+
1833
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1834
+
1835
+ console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
1836
+
1837
+ // Get serverless by name using query parameter
1838
+ const result = await listAllServerless(
1839
+ apiUrl,
1840
+ token,
1841
+ accountId,
1842
+ session,
1843
+ name // Pass name as query parameter
1844
+ );
1845
+
1846
+ if (!result || !Array.isArray(result)) {
1847
+ console.error(
1848
+ chalk.red(
1849
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1850
+ )
1851
+ );
1852
+ return;
1853
+ }
1854
+
1855
+ // Get first element (name is unique)
1856
+ const serverless = result[0];
1857
+
1858
+ if (!serverless) {
1859
+ console.error(chalk.red(`\nāŒ Serverless "${name}" not found.`));
1860
+ console.log(
1861
+ chalk.yellow(
1862
+ "\nUse 'boltic serverless list' to see all serverless functions."
1863
+ )
1864
+ );
1865
+ return;
1866
+ }
1867
+
1868
+ // Display status
1869
+ const runtime = serverless.Config?.Runtime || "code";
1870
+ const typeIcon =
1871
+ runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
1872
+ const status = serverless.Status;
1873
+ const statusColor =
1874
+ status === "running"
1875
+ ? chalk.green
1876
+ : status === "draft"
1877
+ ? chalk.yellow
1878
+ : status === "stopped"
1879
+ ? chalk.red
1880
+ : chalk.gray;
1881
+
1882
+ console.log(chalk.cyan("━".repeat(60)));
1883
+ console.log(chalk.bold("\nšŸ“Š Serverless Status\n"));
1884
+ console.log(
1885
+ chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
1886
+ );
1887
+ console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
1888
+ console.log(
1889
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1890
+ );
1891
+ console.log(chalk.cyan(" Status: ") + statusColor(status));
1892
+
1893
+ if (serverless.Config?.CodeOpts?.Language) {
1894
+ console.log(
1895
+ chalk.cyan(" Language: ") +
1896
+ chalk.white(serverless.Config.CodeOpts.Language)
1897
+ );
1898
+ }
1899
+ if (serverless.Config?.ContainerOpts?.Image) {
1900
+ console.log(
1901
+ chalk.cyan(" Image: ") +
1902
+ chalk.white(serverless.Config.ContainerOpts.Image)
1903
+ );
1904
+ }
1905
+ if (serverless.Config?.Resources) {
1906
+ console.log(
1907
+ chalk.cyan(" Resources: ") +
1908
+ chalk.white(
1909
+ `CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
1910
+ )
1911
+ );
1912
+ }
1913
+ if (serverless.Config?.Scaling) {
1914
+ console.log(
1915
+ chalk.cyan(" Scaling: ") +
1916
+ chalk.white(
1917
+ `Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
1918
+ )
1919
+ );
1920
+ }
1921
+ if (serverless.RegionID) {
1922
+ console.log(
1923
+ chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
1924
+ );
1925
+ }
1926
+ if (serverless.CreatedAt) {
1927
+ console.log(
1928
+ chalk.cyan(" Created: ") +
1929
+ chalk.white(new Date(serverless.CreatedAt).toLocaleString())
1930
+ );
1931
+ }
1932
+ if (serverless.UpdatedAt) {
1933
+ console.log(
1934
+ chalk.cyan(" Updated: ") +
1935
+ chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
1936
+ );
1937
+ }
1938
+
1939
+ console.log();
1940
+ console.log(chalk.cyan("━".repeat(60)));
1941
+ } catch (error) {
1942
+ if (
1943
+ error.message &&
1944
+ error.message.includes("User force closed the prompt")
1945
+ ) {
1946
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1947
+ return;
1948
+ }
1949
+ console.error(
1950
+ chalk.red("\nāŒ An error occurred:"),
1951
+ error.message || "Unknown error"
1952
+ );
1953
+ }
1954
+ }
1955
+
1956
+ export default {
1957
+ execute,
1958
+ };