@boltic/cli 1.0.39 → 1.0.41

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