@boltic/cli 1.0.38 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1942 @@
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.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.yaml with serverlessId inside serverlessConfig
335
+ const serverlessId = response.ID || response.data?.ID || response._id;
336
+ if (serverlessId) {
337
+ const bolticYamlPath = path.join(targetDir, "boltic.yaml");
338
+ let bolticYamlContent = fs.readFileSync(bolticYamlPath, "utf-8");
339
+ // Add serverlessId inside serverlessConfig after the serverlessConfig: line
340
+ bolticYamlContent = bolticYamlContent.replace(
341
+ /^(serverlessConfig:)$/m,
342
+ `$1\n serverlessId: "${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.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.yaml with serverlessId inside serverlessConfig
455
+ const bolticYamlContent = `app: "${name}"
456
+ region: "asia-south1"
457
+ handler: "${HANDLER_MAPPING[language]}"
458
+ language: "${language}/${version}"
459
+
460
+ serverlessConfig:
461
+ serverlessId: "${serverlessId}"
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.yaml"),
496
+ bolticYamlContent
497
+ );
498
+ } catch (err) {
499
+ console.error(chalk.red(`\nāŒ Failed to create boltic.yaml`));
500
+ console.error(chalk.red(`Error: ${err.message}`));
501
+ return;
502
+ }
503
+
504
+ // Check if user has git access by trying ls-remote
505
+ let hasGitAccess = false;
506
+ if (gitSshUrl) {
507
+ console.log(chalk.cyan("\nšŸ” Checking git repository access..."));
508
+ try {
509
+ // Initialize git repo
510
+ execSync(`git init`, { cwd: targetDir, stdio: "pipe" });
511
+ execSync(`git remote add origin ${gitSshUrl}`, {
512
+ cwd: targetDir,
513
+ stdio: "pipe",
514
+ });
515
+ // Try ls-remote to check SSH access
516
+ execSync(`git ls-remote ${gitSshUrl}`, {
517
+ cwd: targetDir,
518
+ stdio: "pipe",
519
+ timeout: 15000,
520
+ });
521
+ hasGitAccess = true;
522
+ } catch (err) {
523
+ hasGitAccess = false;
524
+ }
525
+ }
526
+
527
+ // If user has access, create main branch
528
+ if (hasGitAccess) {
529
+ try {
530
+ console.log(chalk.cyan("šŸ”§ Setting up git branch..."));
531
+ // Create main branch
532
+ execSync(`git checkout -b main`, { cwd: targetDir, stdio: "pipe" });
533
+ console.log(chalk.green("āœ“ Created main branch"));
534
+ } catch (err) {
535
+ // Ignore errors in branch setup, user can do it manually
536
+ console.log(
537
+ chalk.yellow(
538
+ "āš ļø Could not auto-setup git branch. You can set it up manually."
539
+ )
540
+ );
541
+ }
542
+ }
543
+
544
+ // Display success message
545
+ console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
546
+ console.log(
547
+ chalk.green("šŸ“ Git-based serverless project created successfully!")
548
+ );
549
+ console.log();
550
+ console.log(chalk.cyan(" Name: ") + chalk.white(name));
551
+ console.log(chalk.cyan(" Type: ") + chalk.white("git"));
552
+ console.log(
553
+ chalk.cyan(" Language: ") + chalk.white(`${language}/${version}`)
554
+ );
555
+ console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
556
+ console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
557
+
558
+ if (gitSshUrl || gitHttpUrl) {
559
+ console.log();
560
+ console.log(chalk.cyan(" šŸ“¦ Git Repository:"));
561
+ if (gitSshUrl) {
562
+ console.log(chalk.cyan(" SSH URL: ") + chalk.white(gitSshUrl));
563
+ }
564
+ if (gitHttpUrl) {
565
+ console.log(
566
+ chalk.cyan(" Web URL: ") + chalk.white(gitHttpUrl)
567
+ );
568
+ }
569
+ if (gitCloneUrl) {
570
+ console.log(
571
+ chalk.cyan(" Clone URL: ") + chalk.white(gitCloneUrl)
572
+ );
573
+ }
574
+ console.log();
575
+
576
+ if (hasGitAccess) {
577
+ console.log(
578
+ chalk.green("āœ… You have access to the git repository!")
579
+ );
580
+ console.log(chalk.green("āœ… Main branch created!"));
581
+ console.log();
582
+ console.log(
583
+ chalk.yellow("šŸ“ Next steps - Add your code and push:")
584
+ );
585
+ console.log(chalk.dim(" 1. Add your server code to this folder"));
586
+ console.log(chalk.dim(" 2. Commit and push:"));
587
+ console.log(chalk.white(` git add .`));
588
+ console.log(chalk.white(` git commit -m "Initial commit"`));
589
+ console.log(chalk.white(` git push -u origin main`));
590
+ } else {
591
+ console.log(
592
+ chalk.red("āŒ You don't have access to this git repository.")
593
+ );
594
+ console.log(
595
+ chalk.yellow(
596
+ " Please add your SSH key from the Boltic UI to get access."
597
+ )
598
+ );
599
+ console.log();
600
+ console.log(
601
+ chalk.yellow("šŸ“ Once you have access, push your code:")
602
+ );
603
+ console.log(chalk.dim(" 1. Add your code to this folder"));
604
+ console.log(chalk.dim(" 2. Run:"));
605
+ console.log(chalk.white(` git checkout -b main`));
606
+ console.log(chalk.white(` git add .`));
607
+ console.log(chalk.white(` git commit -m "Initial commit"`));
608
+ console.log(chalk.white(` git push -u origin main`));
609
+ }
610
+ } else {
611
+ console.log();
612
+ console.log(chalk.yellow("šŸ“ Next steps:"));
613
+ console.log(chalk.dim(" 1. Add your code to this folder"));
614
+ console.log(chalk.dim(" 2. Configure git remote and push your code"));
615
+ }
616
+ console.log();
617
+ }
618
+
619
+ /**
620
+ * Handle container type serverless creation - creates empty folder with boltic.yaml
621
+ */
622
+ async function handleContainerTypeCreate(name, targetDir) {
623
+ console.log(
624
+ chalk.cyan("\n🐳 Creating container-based serverless project...")
625
+ );
626
+ console.log(chalk.dim(` Type: container`));
627
+
628
+ // Ask for container image URI
629
+ const containerImage = await input({
630
+ message: "Enter container image URI (e.g., docker.io/user/image:tag):",
631
+ validate: (value) => {
632
+ if (!value || value.trim() === "") {
633
+ return "Container image URI is required";
634
+ }
635
+ return true;
636
+ },
637
+ });
638
+
639
+ console.log(chalk.cyan("\nšŸ“¤ Creating serverless function..."));
640
+
641
+ // Get auth credentials
642
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
643
+
644
+ // Build create payload for container type
645
+ const createPayload = {
646
+ Name: name,
647
+ Description: "",
648
+ Runtime: "container",
649
+ PortMap: [],
650
+ Scaling: {
651
+ AutoStop: false,
652
+ Min: 1,
653
+ Max: 1,
654
+ MaxIdleTime: 300,
655
+ },
656
+ Resources: {
657
+ CPU: 0.1,
658
+ MemoryMB: 128,
659
+ MemoryMaxMB: 128,
660
+ },
661
+ Timeout: 60,
662
+ Validations: null,
663
+ ContainerOpts: {
664
+ Image: containerImage.trim(),
665
+ Args: [],
666
+ Command: "",
667
+ },
668
+ };
669
+
670
+ // Call create serverless API
671
+ const response = await publishServerless(
672
+ apiUrl,
673
+ token,
674
+ session,
675
+ createPayload
676
+ );
677
+
678
+ if (!response || !response.ID) {
679
+ console.error(chalk.red("\nāŒ Failed to create serverless function"));
680
+ // Cleanup directory
681
+ try {
682
+ fs.rmSync(targetDir, { recursive: true, force: true });
683
+ } catch {
684
+ // Ignore cleanup errors
685
+ }
686
+ return;
687
+ }
688
+
689
+ const serverlessId = response.ID;
690
+
691
+ // Create boltic.yaml for container type with serverlessId inside serverlessConfig
692
+ const bolticYamlContent = `app: "${name}"
693
+ region: "asia-south1"
694
+
695
+ serverlessConfig:
696
+ serverlessId: "${serverlessId}"
697
+ Name: "${name}"
698
+ Description: ""
699
+ Runtime: "container"
700
+ # Environment variables for your serverless function
701
+ # To add env variables, replace {} with key-value pairs like:
702
+ # Env:
703
+ # API_KEY: "your-api-key"
704
+ Env: {}
705
+ PortMap: []
706
+ Scaling:
707
+ AutoStop: false
708
+ Min: 1
709
+ Max: 1
710
+ MaxIdleTime: 300
711
+ Resources:
712
+ CPU: 0.1
713
+ MemoryMB: 128
714
+ MemoryMaxMB: 128
715
+ Timeout: 60
716
+ Validations: null
717
+ ContainerOpts:
718
+ Image: "${containerImage.trim()}"
719
+ Args: []
720
+ Command: ""
721
+
722
+ build:
723
+ builtin: dockerfile
724
+ ignorefile: .gitignore
725
+ `;
726
+
727
+ try {
728
+ fs.writeFileSync(
729
+ path.join(targetDir, "boltic.yaml"),
730
+ bolticYamlContent
731
+ );
732
+ } catch (err) {
733
+ console.error(chalk.red(`\nāŒ Failed to create boltic.yaml`));
734
+ console.error(chalk.red(`Error: ${err.message}`));
735
+ return;
736
+ }
737
+
738
+ // Display success message for container type
739
+ console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
740
+ console.log(
741
+ chalk.green(
742
+ "🐳 Container-based serverless project created successfully!"
743
+ )
744
+ );
745
+ console.log();
746
+ console.log(chalk.cyan(" Name: ") + chalk.white(name));
747
+ console.log(chalk.cyan(" Type: ") + chalk.white("container"));
748
+ console.log(chalk.cyan(" Image: ") + chalk.white(containerImage.trim()));
749
+ console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
750
+ console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
751
+ console.log();
752
+
753
+ // Poll for serverless status until running
754
+ await pollServerlessStatus(pullServerless, serverlessId, {
755
+ apiUrl,
756
+ token,
757
+ accountId,
758
+ session,
759
+ });
760
+
761
+ console.log(chalk.yellow("šŸ“ Next steps:"));
762
+ console.log(chalk.dim(" 1. To update configuration, edit boltic.yaml"));
763
+ console.log(
764
+ chalk.dim(" 2. To publish changes: boltic serverless publish")
765
+ );
766
+ console.log();
767
+ }
768
+
769
+ /**
770
+ * Handle the publish serverless command
771
+ */
772
+ async function handlePublish(args = []) {
773
+ try {
774
+ console.log(
775
+ "\n" +
776
+ chalk.bgMagenta.black(" šŸš€ SERVERLESS PUBLISH ") +
777
+ chalk.magenta(" Deploy your serverless function\n")
778
+ );
779
+
780
+ // Step 1: Parse CLI arguments
781
+ const parsedArgs = parsePublishArgs(args);
782
+ const { directory } = parsedArgs;
783
+
784
+ // Validate directory exists
785
+ if (!fs.existsSync(directory)) {
786
+ console.error(
787
+ chalk.red(`\nāŒ Directory does not exist: ${directory}`)
788
+ );
789
+ return;
790
+ }
791
+
792
+ // Step 2: Load boltic.yaml config
793
+ const config = loadBolticConfig(directory);
794
+ if (!config) {
795
+ console.error(
796
+ chalk.red("\nāŒ boltic.yaml not found in the directory")
797
+ );
798
+ console.log(
799
+ chalk.yellow(
800
+ "Please run this command from a serverless project directory."
801
+ )
802
+ );
803
+ return;
804
+ }
805
+
806
+ // Step 3: Get app name and language from config
807
+ const appName = config.app;
808
+ const language = config.language; // e.g., "nodejs/20"
809
+ const serverlessConfig = config.serverlessConfig;
810
+ const serverlessId = serverlessConfig?.serverlessId;
811
+
812
+ if (!appName) {
813
+ console.error(chalk.red("\nāŒ App name not found in boltic.yaml"));
814
+ return;
815
+ }
816
+
817
+ if (!language && serverlessConfig?.Runtime !== "container") {
818
+ console.error(chalk.red("\nāŒ Language not found in boltic.yaml"));
819
+ return;
820
+ }
821
+
822
+ console.log(chalk.cyan("šŸ“‹ App Name: ") + chalk.white(appName));
823
+ console.log(chalk.cyan("šŸ“‹ Language: ") + chalk.white(language));
824
+ console.log(
825
+ chalk.cyan("šŸ“‹ Runtime: ") +
826
+ chalk.white(serverlessConfig?.Runtime || "code")
827
+ );
828
+
829
+ // Step 4: Read handler file (only for "code" runtime type)
830
+ const languageBase = parseLanguageFromConfig(language);
831
+ const runtime = serverlessConfig?.Runtime || "code";
832
+ let code = null;
833
+
834
+ if (runtime === "code") {
835
+ code = readHandlerFile(directory, languageBase, config);
836
+
837
+ if (!code) {
838
+ console.error(chalk.red("\nāŒ Handler file not found"));
839
+ const handlerConfig = parseHandlerConfig(
840
+ config.handler,
841
+ languageBase
842
+ );
843
+ console.log(
844
+ chalk.yellow(`Expected handler file: ${handlerConfig.file}`)
845
+ );
846
+ return;
847
+ }
848
+
849
+ console.log(chalk.cyan("šŸ“„ Handler code loaded successfully"));
850
+ }
851
+
852
+ // Step 5: Get auth credentials
853
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
854
+
855
+ let response;
856
+
857
+ // Update existing serverless function
858
+ const payload = buildUpdatePayload(serverlessConfig, language, code);
859
+
860
+ console.log(chalk.cyan("\nšŸ“¤ Updating serverless function..."));
861
+ response = await updateServerless(
862
+ apiUrl,
863
+ token,
864
+ session,
865
+ serverlessId,
866
+ payload
867
+ );
868
+
869
+ if (response) {
870
+ displayPublishSuccessMessage(appName, response);
871
+
872
+ // Poll for serverless status for code and container types only
873
+ if (runtime === "code" || runtime === "container") {
874
+ await pollServerlessStatus(pullServerless, serverlessId, {
875
+ apiUrl,
876
+ token,
877
+ accountId,
878
+ session,
879
+ });
880
+ }
881
+ } else {
882
+ console.error(
883
+ chalk.red(`\nāŒ Failed to publish serverless function`)
884
+ );
885
+ }
886
+ } catch (error) {
887
+ if (
888
+ error.message &&
889
+ error.message.includes("User force closed the prompt")
890
+ ) {
891
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
892
+ return;
893
+ }
894
+ console.error(
895
+ chalk.red("\nāŒ An error occurred:"),
896
+ error.message || "Unknown error"
897
+ );
898
+ }
899
+ }
900
+
901
+ /**
902
+ * Handle the test serverless command
903
+ */
904
+ async function handleTest(args = []) {
905
+ let childProcess = null;
906
+ let language = null;
907
+ let directory = null;
908
+ let retain = false;
909
+
910
+ // Setup cleanup handler
911
+ const cleanup = (signal) => {
912
+ console.log(chalk.yellow(`\n\nāš ļø ${signal} received, cleaning up...`));
913
+
914
+ if (childProcess) {
915
+ childProcess.kill("SIGTERM");
916
+ }
917
+
918
+ if (language && directory) {
919
+ cleanupGeneratedFiles(directory, language, retain);
920
+ }
921
+
922
+ process.exit(0);
923
+ };
924
+
925
+ // Register signal handlers
926
+ process.on("SIGINT", () => cleanup("SIGINT"));
927
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
928
+
929
+ try {
930
+ // Step 1: Parse CLI arguments
931
+ const parsedArgs = parseTestArgs(args);
932
+ let {
933
+ port,
934
+ handlerFile,
935
+ handlerFunction,
936
+ command: customCommand,
937
+ } = parsedArgs;
938
+ language = parsedArgs.language;
939
+ directory = parsedArgs.directory;
940
+ retain = parsedArgs.retain;
941
+
942
+ // Validate directory exists
943
+ if (!fs.existsSync(directory)) {
944
+ console.error(
945
+ chalk.red(`\nāŒ Directory does not exist: ${directory}`)
946
+ );
947
+ return;
948
+ }
949
+
950
+ // Step 2: Load boltic.yaml config
951
+ const config = loadBolticConfig(directory);
952
+ if (!config) {
953
+ console.error(
954
+ chalk.red("\nāŒ boltic.yaml not found in the directory")
955
+ );
956
+ console.log(
957
+ chalk.yellow(
958
+ "You can only test code or container type serverless with boltic.yaml"
959
+ )
960
+ );
961
+ return;
962
+ }
963
+
964
+ // Check if it's a container type serverless
965
+ const runtime = config.serverlessConfig?.Runtime || "code";
966
+ if (runtime === "container") {
967
+ await handleContainerTest(config, directory, port);
968
+ return;
969
+ }
970
+
971
+ // For git type, show message that test is not supported
972
+ if (runtime === "git") {
973
+ console.log(
974
+ chalk.yellow(
975
+ "\nāš ļø Git type serverless test is not supported via CLI."
976
+ )
977
+ );
978
+ console.log(
979
+ chalk.dim(
980
+ "For git type, run your server directly using your project's start command."
981
+ )
982
+ );
983
+ console.log(
984
+ chalk.dim("Example: npm start, python app.py, go run ., etc.")
985
+ );
986
+ return;
987
+ }
988
+
989
+ // Step 3: Determine language (for code type)
990
+ if (!language && config?.language) {
991
+ language = parseLanguageFromConfig(config.language);
992
+ console.log(
993
+ chalk.cyan("šŸ“‹ Using language from boltic.yaml: ") +
994
+ chalk.bold.white(language)
995
+ );
996
+ }
997
+
998
+ if (!language) {
999
+ console.log(
1000
+ chalk.yellow("āš ļø No language specified, auto-detecting...")
1001
+ );
1002
+ language = detectLanguage(directory);
1003
+ }
1004
+
1005
+ if (!language) {
1006
+ console.error(
1007
+ chalk.red(
1008
+ "\nāŒ Could not detect language. Please specify with --language flag."
1009
+ )
1010
+ );
1011
+ console.log(
1012
+ chalk.yellow(
1013
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
1014
+ )
1015
+ );
1016
+ return;
1017
+ }
1018
+
1019
+ // Validate language
1020
+ if (!SUPPORTED_LANGUAGES.includes(language)) {
1021
+ console.error(chalk.red(`\nāŒ Unsupported language: ${language}`));
1022
+ console.log(
1023
+ chalk.yellow(
1024
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
1025
+ )
1026
+ );
1027
+ return;
1028
+ }
1029
+
1030
+ // Step 4: Determine handler file and function
1031
+ if (!handlerFile || !handlerFunction) {
1032
+ const handlerConfig = parseHandlerConfig(config?.handler, language);
1033
+ handlerFile = handlerFile || handlerConfig.file;
1034
+ handlerFunction = handlerFunction || handlerConfig.function;
1035
+ }
1036
+
1037
+ // Verify handler file exists
1038
+ const handlerPath = path.join(directory, handlerFile);
1039
+ if (!fs.existsSync(handlerPath)) {
1040
+ console.error(
1041
+ chalk.red(`\nāŒ Handler file not found: ${handlerPath}`)
1042
+ );
1043
+ console.log(
1044
+ chalk.yellow(
1045
+ "Please specify the correct handler file with --handler-file flag."
1046
+ )
1047
+ );
1048
+ return;
1049
+ }
1050
+
1051
+ // Step 4.1: Detect actual handler function name from code
1052
+ // This handles cases where user might have renamed the function (e.g., handler -> handler1)
1053
+ const handlerCode = fs.readFileSync(handlerPath, "utf8");
1054
+ const detectedFunction = detectHandlerFunctionFromCode(
1055
+ handlerCode,
1056
+ language
1057
+ );
1058
+
1059
+ if (detectedFunction && detectedFunction !== handlerFunction) {
1060
+ console.log(
1061
+ chalk.yellow(`āš ļø Detected handler function: `) +
1062
+ chalk.bold.white(detectedFunction) +
1063
+ chalk.yellow(` (config says: ${handlerFunction})`)
1064
+ );
1065
+ console.log(
1066
+ chalk.cyan(" Using detected function name from code...")
1067
+ );
1068
+ handlerFunction = detectedFunction;
1069
+ }
1070
+
1071
+ console.log(
1072
+ chalk.cyan("šŸ“¦ Handler: ") +
1073
+ chalk.white(`${handlerFile}.${handlerFunction}`)
1074
+ );
1075
+
1076
+ // Step 5: Install dependencies
1077
+ if (language === "nodejs") {
1078
+ const missingDeps = checkNodeDependencies(
1079
+ directory,
1080
+ REQUIRED_DEPENDENCIES.nodejs
1081
+ );
1082
+
1083
+ if (missingDeps.length > 0) {
1084
+ console.log(
1085
+ chalk.yellow(
1086
+ `\nšŸ“¦ Missing dependencies: ${missingDeps.join(", ")}`
1087
+ )
1088
+ );
1089
+ console.log(chalk.cyan(" Installing with --no-save..."));
1090
+
1091
+ try {
1092
+ execSync(`npm install ${missingDeps.join(" ")} --no-save`, {
1093
+ cwd: directory,
1094
+ stdio: "inherit",
1095
+ });
1096
+ console.log(chalk.green(" āœ“ Dependencies installed"));
1097
+ } catch (error) {
1098
+ console.error(
1099
+ chalk.red("\nāŒ Failed to install dependencies")
1100
+ );
1101
+ console.error(chalk.red(`Error: ${error.message}`));
1102
+ return;
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ // Install Python dependencies using virtual environment
1108
+ if (language === "python") {
1109
+ const venvPath = path.join(directory, ".venv");
1110
+ const venvPython = path.join(venvPath, "bin", "python3");
1111
+ const venvPip = path.join(venvPath, "bin", "pip3");
1112
+
1113
+ // Create virtual environment if it doesn't exist
1114
+ if (!fs.existsSync(venvPath)) {
1115
+ console.log(
1116
+ chalk.cyan("\nšŸ“¦ Creating Python virtual environment...")
1117
+ );
1118
+ try {
1119
+ execSync(`python3 -m venv .venv`, {
1120
+ cwd: directory,
1121
+ stdio: "inherit",
1122
+ });
1123
+ console.log(
1124
+ chalk.green(" āœ“ Virtual environment created")
1125
+ );
1126
+ } catch (error) {
1127
+ console.error(
1128
+ chalk.red("\nāŒ Failed to create virtual environment")
1129
+ );
1130
+ console.error(chalk.red(`Error: ${error.message}`));
1131
+ return;
1132
+ }
1133
+ }
1134
+
1135
+ // Install dependencies in the virtual environment
1136
+ const depsToInstall = REQUIRED_DEPENDENCIES.python;
1137
+ console.log(
1138
+ chalk.cyan(
1139
+ `\nšŸ“¦ Installing Python packages: ${depsToInstall.join(", ")}`
1140
+ )
1141
+ );
1142
+
1143
+ try {
1144
+ execSync(`${venvPip} install ${depsToInstall.join(" ")}`, {
1145
+ cwd: directory,
1146
+ stdio: "inherit",
1147
+ });
1148
+ console.log(chalk.green(" āœ“ Python packages installed"));
1149
+ } catch (error) {
1150
+ console.error(
1151
+ chalk.red("\nāŒ Failed to install Python packages")
1152
+ );
1153
+ console.error(chalk.red(`Error: ${error.message}`));
1154
+ return;
1155
+ }
1156
+ }
1157
+
1158
+ // Step 6: Generate test files (wrapper + additional files like pom.xml for Java)
1159
+ console.log(chalk.cyan("\nšŸ“ Generating test files..."));
1160
+
1161
+ // Get app name from config or directory name
1162
+ const appName = config?.app || path.basename(directory);
1163
+
1164
+ const testFiles = generateTestFiles(
1165
+ language,
1166
+ handlerFile,
1167
+ handlerFunction,
1168
+ appName
1169
+ );
1170
+
1171
+ if (!testFiles || testFiles.length === 0) {
1172
+ console.error(
1173
+ chalk.red(
1174
+ `\nāŒ Failed to generate test files for language: ${language}`
1175
+ )
1176
+ );
1177
+ return;
1178
+ }
1179
+
1180
+ // Write all generated files
1181
+ for (const file of testFiles) {
1182
+ const filePath = path.join(directory, file.path);
1183
+
1184
+ // Create directories if needed
1185
+ const fileDir = path.dirname(filePath);
1186
+ if (!fs.existsSync(fileDir)) {
1187
+ fs.mkdirSync(fileDir, { recursive: true });
1188
+ }
1189
+
1190
+ fs.writeFileSync(filePath, file.content, "utf8");
1191
+ console.log(chalk.dim(` Created: ${file.path}`));
1192
+ }
1193
+
1194
+ // Step 7: Determine start command
1195
+ const startCmd = getStartCommand(language, directory, customCommand);
1196
+
1197
+ // Step 8: Set environment variables
1198
+ const env = getTestEnvironmentVariables(port, language);
1199
+
1200
+ // Step 9: Display startup message
1201
+ displayTestStartupMessage(port);
1202
+
1203
+ // Step 10: Start the server
1204
+ childProcess = spawn(startCmd.command, startCmd.args, {
1205
+ cwd: directory,
1206
+ env,
1207
+ stdio: ["inherit", "pipe", "pipe"],
1208
+ shell: process.platform === "win32",
1209
+ });
1210
+
1211
+ // Stream stdout
1212
+ childProcess.stdout.on("data", (data) => {
1213
+ process.stdout.write(chalk.white(data.toString()));
1214
+ });
1215
+
1216
+ // Stream stderr
1217
+ childProcess.stderr.on("data", (data) => {
1218
+ process.stderr.write(chalk.red(data.toString()));
1219
+ });
1220
+
1221
+ // Handle process exit
1222
+ childProcess.on("close", (code) => {
1223
+ console.log(
1224
+ chalk.yellow(`\nšŸ›‘ Server stopped with exit code: ${code}`)
1225
+ );
1226
+ cleanupGeneratedFiles(directory, language, retain);
1227
+ process.exit(code || 0);
1228
+ });
1229
+
1230
+ // Handle process error
1231
+ childProcess.on("error", (error) => {
1232
+ console.error(
1233
+ chalk.red(`\nāŒ Failed to start server: ${error.message}`)
1234
+ );
1235
+
1236
+ if (error.code === "ENOENT") {
1237
+ console.log(
1238
+ chalk.yellow(
1239
+ `\nšŸ’” Hint: Make sure the command "${startCmd.command}" is installed and available in PATH.`
1240
+ )
1241
+ );
1242
+ }
1243
+
1244
+ cleanupGeneratedFiles(directory, language, retain);
1245
+ process.exit(1);
1246
+ });
1247
+ } catch (error) {
1248
+ if (
1249
+ error.message &&
1250
+ error.message.includes("User force closed the prompt")
1251
+ ) {
1252
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1253
+ if (language && directory) {
1254
+ cleanupGeneratedFiles(directory, language, retain);
1255
+ }
1256
+ return;
1257
+ }
1258
+
1259
+ console.error(
1260
+ chalk.red("\nāŒ An error occurred:"),
1261
+ error.message || "Unknown error"
1262
+ );
1263
+
1264
+ if (language && directory) {
1265
+ cleanupGeneratedFiles(directory, language, retain);
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ /**
1271
+ * Handle container type serverless test - runs docker container locally
1272
+ */
1273
+ async function handleContainerTest(config, directory, port) {
1274
+ const containerOpts = config.serverlessConfig?.ContainerOpts;
1275
+ const image = containerOpts?.Image;
1276
+
1277
+ if (!image) {
1278
+ console.error(
1279
+ chalk.red("\nāŒ Container image not found in boltic.yaml")
1280
+ );
1281
+ console.log(
1282
+ chalk.yellow(
1283
+ "Please ensure ContainerOpts.Image is set in serverlessConfig."
1284
+ )
1285
+ );
1286
+ return;
1287
+ }
1288
+
1289
+ console.log(chalk.cyan("\n🐳 Container serverless detected"));
1290
+ console.log(chalk.dim(` Image: ${image}`));
1291
+ console.log(chalk.dim(` Port: ${port}`));
1292
+
1293
+ // Check if Docker is available
1294
+ try {
1295
+ execSync("docker --version", { stdio: "pipe" });
1296
+ } catch (err) {
1297
+ console.error(
1298
+ chalk.red("\nāŒ Docker is not installed or not available in PATH.")
1299
+ );
1300
+ console.log(
1301
+ chalk.yellow(
1302
+ "Please install Docker to test container type serverless."
1303
+ )
1304
+ );
1305
+ return;
1306
+ }
1307
+
1308
+ // Build environment variables from config
1309
+ const envVars = config.serverlessConfig?.Env || {};
1310
+ const envArgs = Object.entries(envVars).flatMap(([key, value]) => [
1311
+ "-e",
1312
+ `${key}=${value}`,
1313
+ ]);
1314
+
1315
+ // Build docker run command
1316
+ const dockerArgs = ["run", "--rm", "-p", `${port}:8080`, ...envArgs, image];
1317
+
1318
+ console.log("\n" + chalk.bgCyan.black(" 🧪 LOCAL CONTAINER TEST ") + "\n");
1319
+ console.log(
1320
+ chalk.green(`šŸš€ Starting container on http://localhost:${port}`)
1321
+ );
1322
+ console.log();
1323
+ console.log(chalk.dim("━".repeat(60)));
1324
+ console.log(chalk.dim(" Press Ctrl+C to stop the container"));
1325
+ console.log(chalk.dim("━".repeat(60)));
1326
+ console.log();
1327
+
1328
+ // Start the container
1329
+ const dockerProcess = spawn("docker", dockerArgs, {
1330
+ cwd: directory,
1331
+ stdio: ["inherit", "pipe", "pipe"],
1332
+ });
1333
+
1334
+ // Stream stdout
1335
+ dockerProcess.stdout.on("data", (data) => {
1336
+ process.stdout.write(chalk.white(data.toString()));
1337
+ });
1338
+
1339
+ // Stream stderr
1340
+ dockerProcess.stderr.on("data", (data) => {
1341
+ process.stderr.write(chalk.yellow(data.toString()));
1342
+ });
1343
+
1344
+ // Handle process exit
1345
+ dockerProcess.on("close", (code) => {
1346
+ console.log(
1347
+ chalk.yellow(`\nšŸ›‘ Container stopped with exit code: ${code}`)
1348
+ );
1349
+ process.exit(code || 0);
1350
+ });
1351
+
1352
+ // Handle process error
1353
+ dockerProcess.on("error", (error) => {
1354
+ console.error(
1355
+ chalk.red(`\nāŒ Failed to start container: ${error.message}`)
1356
+ );
1357
+ if (error.code === "ENOENT") {
1358
+ console.log(
1359
+ chalk.yellow(
1360
+ "\nšŸ’” Hint: Make sure Docker is installed and available in PATH."
1361
+ )
1362
+ );
1363
+ }
1364
+ process.exit(1);
1365
+ });
1366
+
1367
+ // Handle Ctrl+C
1368
+ const cleanup = (signal) => {
1369
+ console.log(
1370
+ chalk.yellow(`\n\nšŸ›‘ Received ${signal}, stopping container...`)
1371
+ );
1372
+ dockerProcess.kill("SIGTERM");
1373
+ };
1374
+
1375
+ process.on("SIGINT", () => cleanup("SIGINT"));
1376
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
1377
+ }
1378
+
1379
+ async function handlePull(args) {
1380
+ console.log(chalk.green("Pulling serverless..."));
1381
+ try {
1382
+ // Parse command line arguments
1383
+ let currentDir = process.cwd();
1384
+ const pathIndex = args.indexOf("--path");
1385
+
1386
+ if (pathIndex !== -1 && args[pathIndex + 1]) {
1387
+ currentDir = args[pathIndex + 1];
1388
+ // Validate the provided path
1389
+ if (!fs.existsSync(currentDir)) {
1390
+ console.error(
1391
+ chalk.red(
1392
+ `Error: The specified path does not exist: ${currentDir}`
1393
+ )
1394
+ );
1395
+ return;
1396
+ }
1397
+ }
1398
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1399
+
1400
+ console.log(
1401
+ chalk.green(
1402
+ "Please select the serverless to pull from the list below:"
1403
+ )
1404
+ );
1405
+
1406
+ const allServerless = await listAllServerless(
1407
+ apiUrl,
1408
+ token,
1409
+ accountId,
1410
+ session
1411
+ );
1412
+ if (!allServerless || !Array.isArray(allServerless)) {
1413
+ console.error(
1414
+ chalk.red(
1415
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1416
+ )
1417
+ );
1418
+ }
1419
+ if (allServerless.length === 0) {
1420
+ console.error(chalk.red("\nāŒ No serverless found."));
1421
+ return;
1422
+ }
1423
+ // Let user select an integration
1424
+ const choices =
1425
+ allServerless.map((serverless) => {
1426
+ const runtime = serverless.Config?.Runtime || "code";
1427
+ const typeIcon =
1428
+ runtime === "git"
1429
+ ? "šŸ“¦"
1430
+ : runtime === "container"
1431
+ ? "🐳"
1432
+ : "šŸ“";
1433
+ const language = serverless.Config?.CodeOpts?.Language;
1434
+ return {
1435
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${serverless.Status}${language ? ` | language: ${language}` : ""}`,
1436
+ value: serverless,
1437
+ };
1438
+ }) || [];
1439
+
1440
+ const selectedServerless = await search({
1441
+ message: "Search and select an serverless to edit:",
1442
+ source: async (term) => {
1443
+ if (!term) return choices;
1444
+ return choices?.filter((choice) =>
1445
+ choice.name.toLowerCase().includes(term.toLowerCase())
1446
+ );
1447
+ },
1448
+ });
1449
+
1450
+ console.log(
1451
+ chalk.cyan("\nSelected serverless:"),
1452
+ selectedServerless.Config.Name
1453
+ );
1454
+ const pulledServerless = await pullServerless(
1455
+ apiUrl,
1456
+ token,
1457
+ accountId,
1458
+ session,
1459
+ selectedServerless.ID
1460
+ );
1461
+ if (!pulledServerless) {
1462
+ console.error(
1463
+ chalk.red(
1464
+ "\nāŒ Failed to fetch serverless details. Please try again later."
1465
+ )
1466
+ );
1467
+ return;
1468
+ }
1469
+ // console.log("selectes serverless : ",pulledServerless)
1470
+
1471
+ // Get the app name, language and type for the folder name
1472
+ const appName =
1473
+ pulledServerless?.Config?.Name || selectedServerless.Config?.Name;
1474
+ const language =
1475
+ pulledServerless?.Config?.CodeOpts?.Language?.split("/")[0] ||
1476
+ "nodejs";
1477
+ const serverlessType = pulledServerless?.Config?.Runtime || "code";
1478
+
1479
+ // Create folder name similar to create command
1480
+ const folderName = appName;
1481
+ const targetDir = path.join(currentDir, folderName);
1482
+
1483
+ // Check if folder already exists
1484
+ if (fs.existsSync(targetDir)) {
1485
+ console.error(
1486
+ chalk.red(
1487
+ `\nāŒ Folder "${folderName}" already exists in ${currentDir}. Please remove it or use a different location.`
1488
+ )
1489
+ );
1490
+ return;
1491
+ }
1492
+
1493
+ // Create the folder
1494
+ fs.mkdirSync(targetDir, { recursive: true });
1495
+ console.log(chalk.cyan(`\nšŸ“ Creating folder: ${folderName}`));
1496
+
1497
+ // Create the files (boltic.yaml with serverlessId and serverlessConfig, handler file with code)
1498
+ try {
1499
+ const result = createPulledServerlessFiles(
1500
+ targetDir,
1501
+ pulledServerless,
1502
+ serverlessType
1503
+ );
1504
+
1505
+ // If there was an error (e.g., no SSH access for git type), don't show success
1506
+ if (result?.error) {
1507
+ return;
1508
+ }
1509
+
1510
+ displayPullSuccessMessage(appName, targetDir);
1511
+ } catch (fileError) {
1512
+ console.error(
1513
+ chalk.red("\nāŒ Failed to create files:"),
1514
+ fileError.message
1515
+ );
1516
+ // Clean up the created folder on error
1517
+ try {
1518
+ fs.rmSync(targetDir, { recursive: true, force: true });
1519
+ } catch (cleanupError) {
1520
+ // Ignore cleanup errors
1521
+ }
1522
+ return;
1523
+ }
1524
+ } catch (error) {
1525
+ if (
1526
+ error.message &&
1527
+ error.message.includes("User force closed the prompt")
1528
+ ) {
1529
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1530
+ return;
1531
+ }
1532
+ // Handle other errors
1533
+ console.error(
1534
+ chalk.red("\nāŒ An error occurred:"),
1535
+ error.message || "Unknown error"
1536
+ );
1537
+ }
1538
+ }
1539
+
1540
+ function showHelp() {
1541
+ console.log(chalk.cyan("\nServerless Commands:\n"));
1542
+ Object.entries(commands).forEach(([cmd, details]) => {
1543
+ console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
1544
+ });
1545
+
1546
+ console.log(chalk.cyan("\nCreate Command Options:\n"));
1547
+ console.log(
1548
+ chalk.bold(" --type, -t") +
1549
+ chalk.dim(" ") +
1550
+ "Serverless type: blueprint, git, or container (prompts if not provided)"
1551
+ );
1552
+ console.log(
1553
+ chalk.bold(" --name, -n") +
1554
+ chalk.dim(" ") +
1555
+ "Name of the serverless function (required, prompts if not provided)"
1556
+ );
1557
+ console.log(
1558
+ chalk.bold(" --language, -l") +
1559
+ chalk.dim(" ") +
1560
+ "Programming language: nodejs, python, golang, java (prompts if not provided)"
1561
+ );
1562
+ console.log(
1563
+ chalk.bold(" --directory, -d") +
1564
+ chalk.dim(" ") +
1565
+ "Directory where to create the project (default: current directory)"
1566
+ );
1567
+
1568
+ console.log(chalk.cyan("\nTest Command Options:\n"));
1569
+ console.log(
1570
+ chalk.bold(" --port, -p") +
1571
+ chalk.dim(" ") +
1572
+ "Port to run the server on (default: 8080)"
1573
+ );
1574
+ console.log(
1575
+ chalk.bold(" --language, -l") +
1576
+ chalk.dim(" ") +
1577
+ "Language (nodejs, python, golang, java) - auto-detected if not specified"
1578
+ );
1579
+ console.log(
1580
+ chalk.bold(" --directory, -d") +
1581
+ chalk.dim(" ") +
1582
+ "Base directory of the project (default: current directory)"
1583
+ );
1584
+
1585
+ console.log(chalk.cyan("\nPublish Command Options:\n"));
1586
+ console.log(
1587
+ chalk.bold(" --directory, -d") +
1588
+ chalk.dim(" ") +
1589
+ "Directory of the serverless project (default: current directory)"
1590
+ );
1591
+
1592
+ console.log(chalk.cyan("\nStatus Command Options:\n"));
1593
+ console.log(
1594
+ chalk.bold(" --name, -n") +
1595
+ chalk.dim(" ") +
1596
+ "Name of the serverless function (prompts if not provided)"
1597
+ );
1598
+
1599
+ console.log(chalk.cyan("\nCreate Examples:\n"));
1600
+ console.log(
1601
+ chalk.dim(
1602
+ " # Interactive mode (will prompt for type, name, and language)"
1603
+ )
1604
+ );
1605
+ console.log(" boltic serverless create\n");
1606
+ console.log(chalk.dim(" # Create blueprint serverless"));
1607
+ console.log(
1608
+ " boltic serverless create --type blueprint --name my-api --language nodejs\n"
1609
+ );
1610
+ console.log(
1611
+ chalk.dim(
1612
+ " # Create git-based serverless (add your code, then publish)"
1613
+ )
1614
+ );
1615
+ console.log(
1616
+ " boltic serverless create --type git --name my-git-func --language python\n"
1617
+ );
1618
+ console.log(chalk.dim(" # Create container-based serverless"));
1619
+ console.log(
1620
+ " boltic serverless create --type container --name my-container --language golang\n"
1621
+ );
1622
+ console.log(chalk.dim(" # With custom directory"));
1623
+ console.log(
1624
+ " boltic serverless create --type blueprint --name my-function --language python --directory ./projects\n"
1625
+ );
1626
+
1627
+ console.log(chalk.cyan("\nTest Examples:\n"));
1628
+ console.log(chalk.dim(" # Basic usage - auto-detect everything"));
1629
+ console.log(" boltic serverless test\n");
1630
+ console.log(chalk.dim(" # Specify port"));
1631
+ console.log(" boltic serverless test --port 3000\n");
1632
+
1633
+ console.log(chalk.cyan("\nPublish Examples:\n"));
1634
+ console.log(chalk.dim(" # Publish from current directory"));
1635
+ console.log(" boltic serverless publish\n");
1636
+ console.log(chalk.dim(" # Publish from specific directory"));
1637
+ console.log(" boltic serverless publish -d ./my-function\n");
1638
+
1639
+ console.log(chalk.cyan("\nList Examples:\n"));
1640
+ console.log(chalk.dim(" # List all serverless functions"));
1641
+ console.log(" boltic serverless list\n");
1642
+
1643
+ console.log(chalk.cyan("\nStatus Examples:\n"));
1644
+ console.log(chalk.dim(" # Get status by name"));
1645
+ console.log(" boltic serverless status -n my-function\n");
1646
+ console.log(chalk.dim(" # Interactive mode (will prompt for name)"));
1647
+ console.log(" boltic serverless status\n");
1648
+ }
1649
+
1650
+ // Execute the serverless command
1651
+ const execute = async (args) => {
1652
+ const subCommand = args[0];
1653
+
1654
+ if (!subCommand) {
1655
+ showHelp();
1656
+ return;
1657
+ }
1658
+
1659
+ if (!commands[subCommand]) {
1660
+ console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
1661
+ showHelp();
1662
+ return;
1663
+ }
1664
+
1665
+ const commandObj = commands[subCommand];
1666
+ await commandObj.action(args.slice(1));
1667
+ };
1668
+
1669
+ async function handleList(args = []) {
1670
+ try {
1671
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1672
+
1673
+ console.log(chalk.cyan("\nšŸ“‹ Fetching serverless functions...\n"));
1674
+
1675
+ const allServerless = await listAllServerless(
1676
+ apiUrl,
1677
+ token,
1678
+ accountId,
1679
+ session
1680
+ );
1681
+
1682
+ if (!allServerless || !Array.isArray(allServerless)) {
1683
+ console.error(
1684
+ chalk.red(
1685
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1686
+ )
1687
+ );
1688
+ return;
1689
+ }
1690
+
1691
+ if (allServerless.length === 0) {
1692
+ console.log(chalk.yellow("No serverless functions found."));
1693
+ return;
1694
+ }
1695
+
1696
+ console.log(
1697
+ chalk.green(`Found ${allServerless.length} serverless function(s):`)
1698
+ );
1699
+ console.log(
1700
+ chalk.dim("Use ↑↓ to scroll, type to search, Ctrl+C to exit\n")
1701
+ );
1702
+
1703
+ // Build choices for the list
1704
+ const choices = allServerless.map((serverless) => {
1705
+ const runtime = serverless.Config?.Runtime || "code";
1706
+ const typeIcon =
1707
+ runtime === "git"
1708
+ ? "šŸ“¦"
1709
+ : runtime === "container"
1710
+ ? "🐳"
1711
+ : "šŸ“";
1712
+ const language = serverless.Config?.CodeOpts?.Language;
1713
+ const status = serverless.Status;
1714
+
1715
+ return {
1716
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${status}${language ? ` | ${language}` : ""} | ID: ${serverless.ID.substring(0, 8)}...`,
1717
+ value: serverless,
1718
+ };
1719
+ });
1720
+
1721
+ // Show interactive scrollable list
1722
+ const selected = await search({
1723
+ message: "Serverless functions (scroll to browse):",
1724
+ source: async (term) => {
1725
+ if (!term) return choices;
1726
+ return choices.filter((choice) =>
1727
+ choice.name.toLowerCase().includes(term.toLowerCase())
1728
+ );
1729
+ },
1730
+ });
1731
+
1732
+ // Show details of selected serverless
1733
+ if (selected) {
1734
+ const runtime = selected.Config?.Runtime || "code";
1735
+ const typeIcon =
1736
+ runtime === "git"
1737
+ ? "šŸ“¦"
1738
+ : runtime === "container"
1739
+ ? "🐳"
1740
+ : "šŸ“";
1741
+
1742
+ console.log("\n" + chalk.cyan("━".repeat(60)));
1743
+ console.log(chalk.bold("\nšŸ“Œ Selected Serverless Details:\n"));
1744
+ console.log(
1745
+ chalk.cyan(" Name: ") + chalk.white(selected.Config.Name)
1746
+ );
1747
+ console.log(chalk.cyan(" ID: ") + chalk.white(selected.ID));
1748
+ console.log(
1749
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1750
+ );
1751
+ console.log(
1752
+ chalk.cyan(" Status: ") + chalk.white(selected.Status)
1753
+ );
1754
+ if (selected.Config?.CodeOpts?.Language) {
1755
+ console.log(
1756
+ chalk.cyan(" Language: ") +
1757
+ chalk.white(selected.Config.CodeOpts.Language)
1758
+ );
1759
+ }
1760
+ if (selected.Config?.ContainerOpts?.Image) {
1761
+ console.log(
1762
+ chalk.cyan(" Image: ") +
1763
+ chalk.white(selected.Config.ContainerOpts.Image)
1764
+ );
1765
+ }
1766
+ console.log(chalk.cyan("━".repeat(60)));
1767
+ console.log(
1768
+ chalk.dim(
1769
+ "\nUse 'boltic serverless pull' to pull this serverless locally."
1770
+ )
1771
+ );
1772
+ }
1773
+ } catch (error) {
1774
+ if (
1775
+ error.message &&
1776
+ error.message.includes("User force closed the prompt")
1777
+ ) {
1778
+ console.log(chalk.yellow("\nāš ļø List closed"));
1779
+ return;
1780
+ }
1781
+ console.error(
1782
+ chalk.red("\nāŒ An error occurred:"),
1783
+ error.message || "Unknown error"
1784
+ );
1785
+ }
1786
+ }
1787
+
1788
+ /**
1789
+ * Handle the status command - show status of a serverless function
1790
+ */
1791
+ async function handleStatus(args = []) {
1792
+ try {
1793
+ // Parse name from args
1794
+ let name = null;
1795
+ const nameIndex = args.indexOf("--name");
1796
+ const shortNameIndex = args.indexOf("-n");
1797
+
1798
+ if (nameIndex !== -1 && args[nameIndex + 1]) {
1799
+ name = args[nameIndex + 1];
1800
+ } else if (shortNameIndex !== -1 && args[shortNameIndex + 1]) {
1801
+ name = args[shortNameIndex + 1];
1802
+ }
1803
+
1804
+ // If name not provided, prompt for it
1805
+ if (!name) {
1806
+ name = await input({
1807
+ message: "Enter serverless name:",
1808
+ validate: (value) => {
1809
+ if (!value || value.trim() === "") {
1810
+ return "Serverless name is required";
1811
+ }
1812
+ return true;
1813
+ },
1814
+ });
1815
+ }
1816
+
1817
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1818
+
1819
+ console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
1820
+
1821
+ // Get serverless by name using query parameter
1822
+ const result = await listAllServerless(
1823
+ apiUrl,
1824
+ token,
1825
+ accountId,
1826
+ session,
1827
+ name // Pass name as query parameter
1828
+ );
1829
+
1830
+ if (!result || !Array.isArray(result)) {
1831
+ console.error(
1832
+ chalk.red(
1833
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1834
+ )
1835
+ );
1836
+ return;
1837
+ }
1838
+
1839
+ // Get first element (name is unique)
1840
+ const serverless = result[0];
1841
+
1842
+ if (!serverless) {
1843
+ console.error(chalk.red(`\nāŒ Serverless "${name}" not found.`));
1844
+ console.log(
1845
+ chalk.yellow(
1846
+ "\nUse 'boltic serverless list' to see all serverless functions."
1847
+ )
1848
+ );
1849
+ return;
1850
+ }
1851
+
1852
+ // Display status
1853
+ const runtime = serverless.Config?.Runtime || "code";
1854
+ const typeIcon =
1855
+ runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
1856
+ const status = serverless.Status;
1857
+ const statusColor =
1858
+ status === "running"
1859
+ ? chalk.green
1860
+ : status === "draft"
1861
+ ? chalk.yellow
1862
+ : status === "stopped"
1863
+ ? chalk.red
1864
+ : chalk.gray;
1865
+
1866
+ console.log(chalk.cyan("━".repeat(60)));
1867
+ console.log(chalk.bold("\nšŸ“Š Serverless Status\n"));
1868
+ console.log(
1869
+ chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
1870
+ );
1871
+ console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
1872
+ console.log(
1873
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1874
+ );
1875
+ console.log(chalk.cyan(" Status: ") + statusColor(status));
1876
+
1877
+ if (serverless.Config?.CodeOpts?.Language) {
1878
+ console.log(
1879
+ chalk.cyan(" Language: ") +
1880
+ chalk.white(serverless.Config.CodeOpts.Language)
1881
+ );
1882
+ }
1883
+ if (serverless.Config?.ContainerOpts?.Image) {
1884
+ console.log(
1885
+ chalk.cyan(" Image: ") +
1886
+ chalk.white(serverless.Config.ContainerOpts.Image)
1887
+ );
1888
+ }
1889
+ if (serverless.Config?.Resources) {
1890
+ console.log(
1891
+ chalk.cyan(" Resources: ") +
1892
+ chalk.white(
1893
+ `CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
1894
+ )
1895
+ );
1896
+ }
1897
+ if (serverless.Config?.Scaling) {
1898
+ console.log(
1899
+ chalk.cyan(" Scaling: ") +
1900
+ chalk.white(
1901
+ `Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
1902
+ )
1903
+ );
1904
+ }
1905
+ if (serverless.RegionID) {
1906
+ console.log(
1907
+ chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
1908
+ );
1909
+ }
1910
+ if (serverless.CreatedAt) {
1911
+ console.log(
1912
+ chalk.cyan(" Created: ") +
1913
+ chalk.white(new Date(serverless.CreatedAt).toLocaleString())
1914
+ );
1915
+ }
1916
+ if (serverless.UpdatedAt) {
1917
+ console.log(
1918
+ chalk.cyan(" Updated: ") +
1919
+ chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
1920
+ );
1921
+ }
1922
+
1923
+ console.log();
1924
+ console.log(chalk.cyan("━".repeat(60)));
1925
+ } catch (error) {
1926
+ if (
1927
+ error.message &&
1928
+ error.message.includes("User force closed the prompt")
1929
+ ) {
1930
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1931
+ return;
1932
+ }
1933
+ console.error(
1934
+ chalk.red("\nāŒ An error occurred:"),
1935
+ error.message || "Unknown error"
1936
+ );
1937
+ }
1938
+ }
1939
+
1940
+ export default {
1941
+ execute,
1942
+ };