@boltic/cli 1.0.41 → 1.0.43

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.
@@ -15,6 +15,7 @@ import {
15
15
  parseTestArgs,
16
16
  parsePublishArgs,
17
17
  createServerlessFiles,
18
+ createGitignore,
18
19
  loadBolticConfig,
19
20
  parseLanguageFromConfig,
20
21
  parseHandlerConfig,
@@ -38,7 +39,11 @@ import {
38
39
  pullServerless,
39
40
  publishServerless,
40
41
  updateServerless,
42
+ getServerlessBuilds,
43
+ getServerlessLogs,
44
+ getBuildLogs,
41
45
  } from "../api/serverless.js";
46
+ import { setVerboseMode } from "../helper/verbose.js";
42
47
 
43
48
  // Define commands and their descriptions
44
49
  const commands = {
@@ -58,10 +63,6 @@ const commands = {
58
63
  description: "Test a serverless function locally",
59
64
  action: handleTest,
60
65
  },
61
- help: {
62
- description: "Show help for serverless commands",
63
- action: showHelp,
64
- },
65
66
  list: {
66
67
  description: "List all serverless functions",
67
68
  action: handleList,
@@ -70,6 +71,22 @@ const commands = {
70
71
  description: "Show status of a serverless function",
71
72
  action: handleStatus,
72
73
  },
74
+ builds: {
75
+ description: "List builds for a serverless function",
76
+ action: handleBuilds,
77
+ },
78
+ logs: {
79
+ description: "Show logs for a serverless function",
80
+ action: handleLogs,
81
+ },
82
+ "build logs": {
83
+ description: "Show logs for a specific build",
84
+ action: handleBuildLogs,
85
+ },
86
+ help: {
87
+ description: "Show help for serverless commands",
88
+ action: showHelp,
89
+ },
73
90
  };
74
91
 
75
92
  // Serverless type choices for dropdown
@@ -92,7 +109,7 @@ async function handleCreate(args = []) {
92
109
 
93
110
  // Step 1: Parse CLI arguments
94
111
  const parsedArgs = parseCreateArgs(args);
95
- let { name, language, directory, type } = parsedArgs;
112
+ let { name, language, directory, type, noGitignore } = parsedArgs;
96
113
 
97
114
  // Step 2: Serverless Type Selection
98
115
  if (!type) {
@@ -204,18 +221,30 @@ async function handleCreate(args = []) {
204
221
  // Branch based on type
205
222
  if (type === "git") {
206
223
  // For git type: create empty folder with boltic.yaml only
207
- await handleGitTypeCreate(name, language, version, targetDir);
224
+ await handleGitTypeCreate(
225
+ name,
226
+ language,
227
+ version,
228
+ targetDir,
229
+ noGitignore
230
+ );
208
231
  return;
209
232
  }
210
233
 
211
234
  if (type === "container") {
212
235
  // For container type: ask for image and create serverless
213
- await handleContainerTypeCreate(name, targetDir);
236
+ await handleContainerTypeCreate(name, targetDir, noGitignore);
214
237
  return;
215
238
  }
216
239
 
217
240
  // For code type: create full template files and call create API
218
- await handleCodeTypeCreate(name, language, version, targetDir);
241
+ await handleCodeTypeCreate(
242
+ name,
243
+ language,
244
+ version,
245
+ targetDir,
246
+ noGitignore
247
+ );
219
248
  } catch (error) {
220
249
  if (
221
250
  error.message &&
@@ -232,10 +261,87 @@ async function handleCreate(args = []) {
232
261
  }
233
262
  }
234
263
 
264
+ /**
265
+ * Check if a serverless function with the given name already exists
266
+ * @returns {Object|null} The existing serverless object if found, null otherwise
267
+ */
268
+ async function checkServerlessExists(name) {
269
+ const env = await getCurrentEnv();
270
+ if (!env || !env.token || !env.session) {
271
+ return null; // Can't check without auth, let the create call handle auth error
272
+ }
273
+
274
+ const { apiUrl, token, accountId, session } = env;
275
+
276
+ try {
277
+ const allServerless = await listAllServerless(
278
+ apiUrl,
279
+ token,
280
+ accountId,
281
+ session,
282
+ name // Use query parameter to search by name
283
+ );
284
+
285
+ if (allServerless && Array.isArray(allServerless)) {
286
+ // Find exact match by name (case-insensitive)
287
+ const existing = allServerless.find(
288
+ (s) => s.Name && s.Name.toLowerCase() === name.toLowerCase()
289
+ );
290
+ return existing || null;
291
+ }
292
+ } catch {
293
+ // If API call fails, let the create call handle it
294
+ return null;
295
+ }
296
+
297
+ return null;
298
+ }
299
+
300
+ /**
301
+ * Display message when serverless already exists and suggest pull command
302
+ */
303
+ function displayServerlessExistsMessage(name, existing) {
304
+ console.log(
305
+ chalk.yellow(
306
+ `\nāš ļø A serverless function named "${name}" already exists.`
307
+ )
308
+ );
309
+ console.log(chalk.dim(` ID: ${existing.ID || existing._id}`));
310
+ if (existing.Status) {
311
+ console.log(chalk.dim(` Status: ${existing.Status}`));
312
+ }
313
+ console.log();
314
+ console.log(chalk.cyan("To pull the existing serverless function, run:"));
315
+ console.log(chalk.green(` boltic serverless pull --name ${name}`));
316
+ console.log();
317
+ console.log(chalk.dim("Or use a different name:"));
318
+ console.log(chalk.dim(` boltic serverless create --name <new-name> ...`));
319
+ console.log();
320
+ }
321
+
235
322
  /**
236
323
  * Handle code type serverless creation - creates folder with template files and calls create API
237
324
  */
238
- async function handleCodeTypeCreate(name, language, version, targetDir) {
325
+ async function handleCodeTypeCreate(
326
+ name,
327
+ language,
328
+ version,
329
+ targetDir,
330
+ noGitignore = false
331
+ ) {
332
+ // Check if serverless with this name already exists
333
+ const existingServerless = await checkServerlessExists(name);
334
+ if (existingServerless) {
335
+ displayServerlessExistsMessage(name, existingServerless);
336
+ // Cleanup the created directory
337
+ try {
338
+ fs.rmSync(targetDir, { recursive: true, force: true });
339
+ } catch {
340
+ // Ignore cleanup errors
341
+ }
342
+ return;
343
+ }
344
+
239
345
  const templateContext = {
240
346
  AppSlug: name,
241
347
  Language: `${language}/${version}`,
@@ -263,6 +369,14 @@ async function handleCodeTypeCreate(name, language, version, targetDir) {
263
369
  return;
264
370
  }
265
371
 
372
+ // Create .gitignore file unless --no-gitignore flag is set
373
+ if (!noGitignore) {
374
+ const gitignoreCreated = createGitignore(targetDir, language);
375
+ if (gitignoreCreated) {
376
+ console.log(chalk.dim(` Created .gitignore for ${language}`));
377
+ }
378
+ }
379
+
266
380
  // Get authentication credentials
267
381
  const env = await getCurrentEnv();
268
382
  if (!env || !env.token || !env.session) {
@@ -383,7 +497,26 @@ async function handleCodeTypeCreate(name, language, version, targetDir) {
383
497
  /**
384
498
  * Handle git type serverless creation - creates serverless on server and clones the repo
385
499
  */
386
- async function handleGitTypeCreate(name, language, version, targetDir) {
500
+ async function handleGitTypeCreate(
501
+ name,
502
+ language,
503
+ version,
504
+ targetDir,
505
+ noGitignore = false
506
+ ) {
507
+ // Check if serverless with this name already exists
508
+ const existingServerless = await checkServerlessExists(name);
509
+ if (existingServerless) {
510
+ displayServerlessExistsMessage(name, existingServerless);
511
+ // Cleanup the created directory
512
+ try {
513
+ fs.rmSync(targetDir, { recursive: true, force: true });
514
+ } catch {
515
+ // Ignore cleanup errors
516
+ }
517
+ return;
518
+ }
519
+
387
520
  console.log(chalk.cyan("\nšŸ“ Creating git-based serverless project..."));
388
521
  console.log(chalk.dim(` Type: git`));
389
522
  console.log(chalk.dim(` Language: ${language}/${version}`));
@@ -523,6 +656,14 @@ serverlessConfig:
523
656
  }
524
657
  }
525
658
 
659
+ // Create .gitignore file unless --no-gitignore flag is set
660
+ if (!noGitignore) {
661
+ const gitignoreCreated = createGitignore(targetDir, language);
662
+ if (gitignoreCreated) {
663
+ console.log(chalk.dim(` Created .gitignore for ${language}`));
664
+ }
665
+ }
666
+
526
667
  // Display success message
527
668
  console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
528
669
  console.log(
@@ -608,7 +749,20 @@ serverlessConfig:
608
749
  /**
609
750
  * Handle container type serverless creation - creates empty folder with boltic.yaml
610
751
  */
611
- async function handleContainerTypeCreate(name, targetDir) {
752
+ async function handleContainerTypeCreate(name, targetDir, noGitignore = false) {
753
+ // Check if serverless with this name already exists
754
+ const existingServerless = await checkServerlessExists(name);
755
+ if (existingServerless) {
756
+ displayServerlessExistsMessage(name, existingServerless);
757
+ // Cleanup the created directory
758
+ try {
759
+ fs.rmSync(targetDir, { recursive: true, force: true });
760
+ } catch {
761
+ // Ignore cleanup errors
762
+ }
763
+ return;
764
+ }
765
+
612
766
  console.log(
613
767
  chalk.cyan("\n🐳 Creating container-based serverless project...")
614
768
  );
@@ -724,6 +878,14 @@ build:
724
878
  return;
725
879
  }
726
880
 
881
+ // Create .gitignore file unless --no-gitignore flag is set
882
+ if (!noGitignore) {
883
+ const gitignoreCreated = createGitignore(targetDir, "container");
884
+ if (gitignoreCreated) {
885
+ console.log(chalk.dim(` Created .gitignore`));
886
+ }
887
+ }
888
+
727
889
  // Display success message for container type
728
890
  console.log("\n" + chalk.bgGreen.black(" āœ“ CREATED ") + "\n");
729
891
  console.log(
@@ -768,7 +930,12 @@ async function handlePublish(args = []) {
768
930
 
769
931
  // Step 1: Parse CLI arguments
770
932
  const parsedArgs = parsePublishArgs(args);
771
- const { directory } = parsedArgs;
933
+ const { directory, verbose } = parsedArgs;
934
+
935
+ // Enable verbose mode if requested
936
+ if (verbose) {
937
+ setVerboseMode(true);
938
+ }
772
939
 
773
940
  // Validate directory exists
774
941
  if (!fs.existsSync(directory)) {
@@ -821,10 +988,28 @@ async function handlePublish(args = []) {
821
988
  let code = null;
822
989
  if (runtime === "git") {
823
990
  console.log(
824
- chalk.red("\nšŸ“„ Git type serverless does not support publish")
991
+ chalk.yellow(
992
+ "\nšŸ“¦ Git-based serverless deploys via git push, not publish."
993
+ )
994
+ );
995
+ console.log(chalk.cyan("\nTo deploy your changes:\n"));
996
+ console.log(chalk.white(" # Stage your changes"));
997
+ console.log(chalk.green(" git add .\n"));
998
+ console.log(chalk.white(" # Commit your changes"));
999
+ console.log(
1000
+ chalk.green(' git commit -m "Update serverless function"\n')
1001
+ );
1002
+ console.log(chalk.white(" # Push to deploy"));
1003
+ console.log(chalk.green(" git push origin main\n"));
1004
+ console.log(
1005
+ chalk.dim(
1006
+ "The serverless will automatically build and deploy after push."
1007
+ )
825
1008
  );
826
1009
  console.log(
827
- chalk.yellow("Please publish using git push origin main")
1010
+ chalk.dim(
1011
+ `Monitor status with: boltic serverless status --name ${appName} --follow\n`
1012
+ )
828
1013
  );
829
1014
  return;
830
1015
  }
@@ -1105,7 +1290,6 @@ async function handleTest(args = []) {
1105
1290
  // Install Python dependencies using virtual environment
1106
1291
  if (language === "python") {
1107
1292
  const venvPath = path.join(directory, ".venv");
1108
- const venvPython = path.join(venvPath, "bin", "python3");
1109
1293
  const venvPip = path.join(venvPath, "bin", "pip3");
1110
1294
 
1111
1295
  // Create virtual environment if it doesn't exist
@@ -1379,27 +1563,32 @@ async function handlePull(args) {
1379
1563
  try {
1380
1564
  // Parse command line arguments
1381
1565
  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;
1566
+ let serverlessName = null;
1567
+
1568
+ for (let i = 0; i < args.length; i++) {
1569
+ const arg = args[i];
1570
+ const nextArg = args[i + 1];
1571
+
1572
+ if (arg === "--path" && nextArg) {
1573
+ currentDir = nextArg;
1574
+ i++;
1575
+ } else if ((arg === "--name" || arg === "-n") && nextArg) {
1576
+ serverlessName = nextArg;
1577
+ i++;
1394
1578
  }
1395
1579
  }
1396
- const { apiUrl, token, accountId, session } = await getCurrentEnv();
1397
1580
 
1398
- console.log(
1399
- chalk.green(
1400
- "Please select the serverless to pull from the list below:"
1401
- )
1402
- );
1581
+ // Validate the provided path
1582
+ if (currentDir !== process.cwd() && !fs.existsSync(currentDir)) {
1583
+ console.error(
1584
+ chalk.red(
1585
+ `Error: The specified path does not exist: ${currentDir}`
1586
+ )
1587
+ );
1588
+ return;
1589
+ }
1590
+
1591
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1403
1592
 
1404
1593
  const allServerless = await listAllServerless(
1405
1594
  apiUrl,
@@ -1413,42 +1602,85 @@ async function handlePull(args) {
1413
1602
  "\nāŒ Failed to fetch serverless: Invalid response format"
1414
1603
  )
1415
1604
  );
1605
+ return;
1416
1606
  }
1417
1607
  if (allServerless.length === 0) {
1418
1608
  console.error(chalk.red("\nāŒ No serverless found."));
1419
1609
  return;
1420
1610
  }
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
1611
 
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())
1612
+ let selectedServerless;
1613
+
1614
+ // If name is provided, find exact match
1615
+ if (serverlessName) {
1616
+ selectedServerless = allServerless.find(
1617
+ (s) =>
1618
+ s.Config?.Name?.toLowerCase() ===
1619
+ serverlessName.toLowerCase()
1620
+ );
1621
+
1622
+ if (!selectedServerless) {
1623
+ console.error(
1624
+ chalk.red(`\nāŒ Serverless "${serverlessName}" not found.`)
1444
1625
  );
1445
- },
1446
- });
1626
+ console.log(chalk.yellow("\nAvailable serverless functions:"));
1627
+ allServerless.slice(0, 5).forEach((s) => {
1628
+ console.log(chalk.dim(` - ${s.Config?.Name}`));
1629
+ });
1630
+ if (allServerless.length > 5) {
1631
+ console.log(
1632
+ chalk.dim(` ... and ${allServerless.length - 5} more`)
1633
+ );
1634
+ }
1635
+ console.log(
1636
+ chalk.yellow("\nRun 'boltic serverless list' to see all.")
1637
+ );
1638
+ return;
1639
+ }
1447
1640
 
1448
- console.log(
1449
- chalk.cyan("\nSelected serverless:"),
1450
- selectedServerless.Config.Name
1451
- );
1641
+ console.log(
1642
+ chalk.cyan("Selected serverless:"),
1643
+ selectedServerless.Config.Name
1644
+ );
1645
+ } else {
1646
+ // Interactive selection
1647
+ console.log(
1648
+ chalk.green(
1649
+ "Please select the serverless to pull from the list below:"
1650
+ )
1651
+ );
1652
+
1653
+ const choices =
1654
+ allServerless.map((serverless) => {
1655
+ const runtime = serverless.Config?.Runtime || "code";
1656
+ const typeIcon =
1657
+ runtime === "git"
1658
+ ? "šŸ“¦"
1659
+ : runtime === "container"
1660
+ ? "🐳"
1661
+ : "šŸ“";
1662
+ const language = serverless.Config?.CodeOpts?.Language;
1663
+ return {
1664
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${serverless.Status}${language ? ` | language: ${language}` : ""}`,
1665
+ value: serverless,
1666
+ };
1667
+ }) || [];
1668
+
1669
+ selectedServerless = await search({
1670
+ message: "Search and select an serverless to edit:",
1671
+ source: async (term) => {
1672
+ if (!term) return choices;
1673
+ return choices?.filter((choice) =>
1674
+ choice.name.toLowerCase().includes(term.toLowerCase())
1675
+ );
1676
+ },
1677
+ });
1678
+
1679
+ console.log(
1680
+ chalk.cyan("\nSelected serverless:"),
1681
+ selectedServerless.Config.Name
1682
+ );
1683
+ }
1452
1684
  const pulledServerless = await pullServerless(
1453
1685
  apiUrl,
1454
1686
  token,
@@ -1466,12 +1698,9 @@ async function handlePull(args) {
1466
1698
  }
1467
1699
  // console.log("selectes serverless : ",pulledServerless)
1468
1700
 
1469
- // Get the app name, language and type for the folder name
1701
+ // Get the app name and type for the folder name
1470
1702
  const appName =
1471
1703
  pulledServerless?.Config?.Name || selectedServerless.Config?.Name;
1472
- const language =
1473
- pulledServerless?.Config?.CodeOpts?.Language?.split("/")[0] ||
1474
- "nodejs";
1475
1704
  const serverlessType = pulledServerless?.Config?.Runtime || "code";
1476
1705
 
1477
1706
  // Create folder name similar to create command
@@ -1538,133 +1767,185 @@ async function handlePull(args) {
1538
1767
  function showHelp() {
1539
1768
  console.log(chalk.cyan("\nServerless Commands:\n"));
1540
1769
  Object.entries(commands).forEach(([cmd, details]) => {
1541
- console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
1770
+ console.log(chalk.bold(` ${cmd.padEnd(12)}`) + details.description);
1542
1771
  });
1543
1772
 
1544
- console.log(chalk.cyan("\nCreate Command Options:\n"));
1773
+ console.log(chalk.cyan("\nGlobal Options:\n"));
1774
+ console.log(
1775
+ chalk.bold(" --help, -h".padEnd(20)) + "Show help for a command"
1776
+ );
1777
+
1778
+ console.log(chalk.cyan("\nCreate Options:\n"));
1545
1779
  console.log(
1546
- chalk.bold(" --type, -t") +
1547
- chalk.dim(" ") +
1548
- "Serverless type: blueprint, git, or container (prompts if not provided)"
1780
+ chalk.bold(" --type, -t".padEnd(20)) +
1781
+ "Serverless type: blueprint, git, or container"
1549
1782
  );
1550
1783
  console.log(
1551
- chalk.bold(" --name, -n") +
1552
- chalk.dim(" ") +
1553
- "Name of the serverless function (required, prompts if not provided)"
1784
+ chalk.bold(" --name, -n".padEnd(20)) +
1785
+ "Name of the serverless function"
1554
1786
  );
1555
1787
  console.log(
1556
- chalk.bold(" --language, -l") +
1557
- chalk.dim(" ") +
1558
- "Programming language: nodejs, python, golang, java (prompts if not provided)"
1788
+ chalk.bold(" --language, -l".padEnd(20)) +
1789
+ "Programming language: nodejs, python, golang, java"
1559
1790
  );
1560
1791
  console.log(
1561
- chalk.bold(" --directory, -d") +
1562
- chalk.dim(" ") +
1563
- "Directory where to create the project (default: current directory)"
1792
+ chalk.bold(" --directory, -d".padEnd(20)) +
1793
+ "Directory for the project (default: current)"
1564
1794
  );
1565
1795
 
1566
- console.log(chalk.cyan("\nTest Command Options:\n"));
1796
+ console.log(chalk.cyan("\nTest Options:\n"));
1567
1797
  console.log(
1568
- chalk.bold(" --port, -p") +
1569
- chalk.dim(" ") +
1798
+ chalk.bold(" --port, -p".padEnd(20)) +
1570
1799
  "Port to run the server on (default: 8080)"
1571
1800
  );
1572
1801
  console.log(
1573
- chalk.bold(" --language, -l") +
1574
- chalk.dim(" ") +
1575
- "Language (nodejs, python, golang, java) - auto-detected if not specified"
1802
+ chalk.bold(" --language, -l".padEnd(20)) +
1803
+ "Language (auto-detected if not specified)"
1576
1804
  );
1577
1805
  console.log(
1578
- chalk.bold(" --directory, -d") +
1579
- chalk.dim(" ") +
1580
- "Base directory of the project (default: current directory)"
1806
+ chalk.bold(" --directory, -d".padEnd(20)) +
1807
+ "Base directory of the project"
1581
1808
  );
1582
1809
 
1583
- console.log(chalk.cyan("\nPublish Command Options:\n"));
1810
+ console.log(chalk.cyan("\nPublish Options:\n"));
1584
1811
  console.log(
1585
- chalk.bold(" --directory, -d") +
1586
- chalk.dim(" ") +
1587
- "Directory of the serverless project (default: current directory)"
1812
+ chalk.bold(" --directory, -d".padEnd(20)) +
1813
+ "Directory of the serverless project"
1588
1814
  );
1589
1815
 
1590
- console.log(chalk.cyan("\nStatus Command Options:\n"));
1816
+ console.log(chalk.cyan("\nStatus Options:\n"));
1591
1817
  console.log(
1592
- chalk.bold(" --name, -n") +
1593
- chalk.dim(" ") +
1594
- "Name of the serverless function (prompts if not provided)"
1818
+ chalk.bold(" --name, -n".padEnd(20)) +
1819
+ "Name of the serverless function"
1820
+ );
1821
+ console.log(
1822
+ chalk.bold(" --follow, -f".padEnd(20)) +
1823
+ "Poll until status is running, failed, or degraded"
1595
1824
  );
1596
1825
 
1597
- console.log(chalk.cyan("\nCreate Examples:\n"));
1826
+ console.log(chalk.cyan("\nBuilds Options:\n"));
1598
1827
  console.log(
1599
- chalk.dim(
1600
- " # Interactive mode (will prompt for type, name, and language)"
1601
- )
1828
+ chalk.bold(" --name, -n".padEnd(20)) +
1829
+ "Name of the serverless function"
1602
1830
  );
1603
- console.log(" boltic serverless create\n");
1604
- console.log(chalk.dim(" # Create blueprint serverless"));
1831
+
1832
+ console.log(chalk.cyan("\nLogs Options:\n"));
1605
1833
  console.log(
1606
- " boltic serverless create --type blueprint --name my-api --language nodejs\n"
1834
+ chalk.bold(" --name, -n".padEnd(20)) +
1835
+ "Name of the serverless function"
1607
1836
  );
1608
1837
  console.log(
1609
- chalk.dim(
1610
- " # Create git-based serverless (add your code, then publish)"
1611
- )
1838
+ chalk.bold(" --follow, -f".padEnd(20)) + "Follow logs in real-time"
1839
+ );
1840
+ console.log(
1841
+ chalk.bold(" --lines, -l".padEnd(20)) +
1842
+ "Number of lines to show (default: 100)"
1612
1843
  );
1844
+
1845
+ console.log(chalk.cyan("\nBuild Logs Options:\n"));
1613
1846
  console.log(
1614
- " boltic serverless create --type git --name my-git-func --language python\n"
1847
+ chalk.bold(" --name, -n".padEnd(20)) +
1848
+ "Name of the serverless function"
1615
1849
  );
1616
- console.log(chalk.dim(" # Create container-based serverless"));
1617
1850
  console.log(
1618
- " boltic serverless create --type container --name my-container --language golang\n"
1851
+ chalk.bold(" --build, -b".padEnd(20)) +
1852
+ "Build ID (prompts if not provided)"
1619
1853
  );
1620
- console.log(chalk.dim(" # With custom directory"));
1854
+
1855
+ console.log(chalk.cyan("\nExamples:\n"));
1856
+
1857
+ console.log(chalk.dim(" # Create a blueprint serverless"));
1621
1858
  console.log(
1622
- " boltic serverless create --type blueprint --name my-function --language python --directory ./projects\n"
1859
+ " boltic serverless create -t blueprint -n my-api -l nodejs\n"
1623
1860
  );
1624
1861
 
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");
1862
+ console.log(chalk.dim(" # Test locally on port 3000"));
1863
+ console.log(" boltic serverless test -p 3000\n");
1630
1864
 
1631
- console.log(chalk.cyan("\nPublish Examples:\n"));
1632
1865
  console.log(chalk.dim(" # Publish from current directory"));
1633
1866
  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
1867
 
1637
- console.log(chalk.cyan("\nList Examples:\n"));
1638
1868
  console.log(chalk.dim(" # List all serverless functions"));
1639
1869
  console.log(" boltic serverless list\n");
1640
1870
 
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");
1871
+ console.log(chalk.dim(" # Check status with polling"));
1872
+ console.log(" boltic serverless status -n my-function --follow\n");
1873
+
1874
+ console.log(chalk.dim(" # View builds for a serverless"));
1875
+ console.log(" boltic serverless builds -n my-function\n");
1876
+
1877
+ console.log(chalk.dim(" # View runtime logs"));
1878
+ console.log(" boltic serverless logs -n my-function -f\n");
1879
+
1880
+ console.log(chalk.dim(" # View build logs"));
1881
+ console.log(" boltic serverless build logs -n my-function\n");
1646
1882
  }
1647
1883
 
1648
1884
  // Execute the serverless command
1649
1885
  const execute = async (args) => {
1650
- const subCommand = args[0];
1651
-
1652
- if (!subCommand) {
1886
+ let subCommand = args[0];
1887
+ let argsToPass = args.slice(1);
1888
+
1889
+ // Handle help flags
1890
+ if (
1891
+ !subCommand ||
1892
+ subCommand === "--help" ||
1893
+ subCommand === "-h" ||
1894
+ args.includes("--help") ||
1895
+ args.includes("-h")
1896
+ ) {
1653
1897
  showHelp();
1654
1898
  return;
1655
1899
  }
1656
1900
 
1901
+ // Handle two-word commands like "build logs"
1902
+ if (subCommand === "build" && args[1] === "logs") {
1903
+ subCommand = "build logs";
1904
+ argsToPass = args.slice(2);
1905
+ }
1906
+
1657
1907
  if (!commands[subCommand]) {
1658
- console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
1908
+ console.log(chalk.red(`Unknown serverless command: "${subCommand}"\n`));
1659
1909
  showHelp();
1660
1910
  return;
1661
1911
  }
1662
1912
 
1663
1913
  const commandObj = commands[subCommand];
1664
- await commandObj.action(args.slice(1));
1914
+ await commandObj.action(argsToPass);
1665
1915
  };
1666
1916
 
1667
- async function handleList(args = []) {
1917
+ /**
1918
+ * Get the URL for a serverless function
1919
+ */
1920
+ function getServerlessUrl(serverless) {
1921
+ const appDomain = serverless.AppDomain?.[0];
1922
+ if (appDomain) {
1923
+ return `https://${appDomain.DomainName}.${appDomain.BaseUrl || "serverless.boltic.app"}`;
1924
+ }
1925
+ return null;
1926
+ }
1927
+
1928
+ /**
1929
+ * Get status color for display
1930
+ */
1931
+ function getStatusColor(status) {
1932
+ switch (status) {
1933
+ case "running":
1934
+ return chalk.green;
1935
+ case "draft":
1936
+ case "building":
1937
+ case "pending":
1938
+ return chalk.yellow;
1939
+ case "stopped":
1940
+ case "failed":
1941
+ case "degraded":
1942
+ return chalk.red;
1943
+ default:
1944
+ return chalk.gray;
1945
+ }
1946
+ }
1947
+
1948
+ async function handleList(_args = []) {
1668
1949
  try {
1669
1950
  const { apiUrl, token, accountId, session } = await getCurrentEnv();
1670
1951
 
@@ -1709,9 +1990,10 @@ async function handleList(args = []) {
1709
1990
  : "šŸ“";
1710
1991
  const language = serverless.Config?.CodeOpts?.Language;
1711
1992
  const status = serverless.Status;
1993
+ const url = getServerlessUrl(serverless);
1712
1994
 
1713
1995
  return {
1714
- name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${status}${language ? ` | ${language}` : ""} | ID: ${serverless.ID.substring(0, 8)}...`,
1996
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | ${status}${language ? ` | ${language}` : ""}${url ? ` | ${url}` : ""}`,
1715
1997
  value: serverless,
1716
1998
  };
1717
1999
  });
@@ -1729,44 +2011,7 @@ async function handleList(args = []) {
1729
2011
 
1730
2012
  // Show details of selected serverless
1731
2013
  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
- );
2014
+ displayServerlessDetails(selected);
1770
2015
  }
1771
2016
  } catch (error) {
1772
2017
  if (
@@ -1784,142 +2029,1000 @@ async function handleList(args = []) {
1784
2029
  }
1785
2030
 
1786
2031
  /**
1787
- * Handle the status command - show status of a serverless function
2032
+ * Display detailed information about a serverless function
1788
2033
  */
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();
2034
+ function displayServerlessDetails(serverless) {
2035
+ const runtime = serverless.Config?.Runtime || "code";
2036
+ const typeIcon =
2037
+ runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
2038
+ const status = serverless.Status;
2039
+ const statusColor = getStatusColor(status);
2040
+ const url = getServerlessUrl(serverless);
2041
+
2042
+ console.log("\n" + chalk.cyan("━".repeat(60)));
2043
+ console.log(chalk.bold("\nšŸ“Š Serverless Details\n"));
2044
+ console.log(chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name));
2045
+ console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
2046
+ console.log(
2047
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
2048
+ );
2049
+ console.log(chalk.cyan(" Status: ") + statusColor(status));
1816
2050
 
1817
- console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
2051
+ if (url) {
2052
+ console.log(chalk.cyan(" URL: ") + chalk.white.bold(url));
2053
+ }
1818
2054
 
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
2055
+ if (serverless.Config?.CodeOpts?.Language) {
2056
+ console.log(
2057
+ chalk.cyan(" Language: ") +
2058
+ chalk.white(serverless.Config.CodeOpts.Language)
1826
2059
  );
1827
-
1828
- if (!result || !Array.isArray(result)) {
1829
- console.error(
1830
- chalk.red(
1831
- "\nāŒ Failed to fetch serverless: Invalid response format"
2060
+ }
2061
+ if (serverless.Config?.ContainerOpts?.Image) {
2062
+ console.log(
2063
+ chalk.cyan(" Image: ") +
2064
+ chalk.white(serverless.Config.ContainerOpts.Image)
2065
+ );
2066
+ }
2067
+ if (serverless.Config?.Resources) {
2068
+ console.log(
2069
+ chalk.cyan(" Resources: ") +
2070
+ chalk.white(
2071
+ `CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
1832
2072
  )
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."
2073
+ );
2074
+ }
2075
+ if (serverless.Config?.Scaling) {
2076
+ console.log(
2077
+ chalk.cyan(" Scaling: ") +
2078
+ chalk.white(
2079
+ `Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
1845
2080
  )
2081
+ );
2082
+ }
2083
+ if (serverless.RegionID) {
2084
+ console.log(
2085
+ chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
2086
+ );
2087
+ }
2088
+ if (serverless.CreatedAt) {
2089
+ console.log(
2090
+ chalk.cyan(" Created: ") +
2091
+ chalk.white(new Date(serverless.CreatedAt).toLocaleString())
2092
+ );
2093
+ }
2094
+ if (serverless.UpdatedAt) {
2095
+ console.log(
2096
+ chalk.cyan(" Updated: ") +
2097
+ chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
2098
+ );
2099
+ }
2100
+
2101
+ console.log();
2102
+ console.log(chalk.cyan("━".repeat(60)));
2103
+ console.log(
2104
+ chalk.dim(
2105
+ "\nTip: Use 'boltic serverless status -n <name> --follow' to poll for status changes."
2106
+ )
2107
+ );
2108
+ }
2109
+
2110
+ /**
2111
+ * Parse status command arguments
2112
+ */
2113
+ function parseStatusArgs(args) {
2114
+ const parsed = {
2115
+ name: null,
2116
+ watch: false,
2117
+ verbose: false,
2118
+ timeout: -1, // -1 means infinite
2119
+ };
2120
+
2121
+ for (let i = 0; i < args.length; i++) {
2122
+ const arg = args[i];
2123
+ const nextArg = args[i + 1];
2124
+
2125
+ if ((arg === "--name" || arg === "-n") && nextArg) {
2126
+ parsed.name = nextArg;
2127
+ i++;
2128
+ } else if (
2129
+ arg === "--follow" ||
2130
+ arg === "-f" ||
2131
+ arg === "--watch" ||
2132
+ arg === "-w"
2133
+ ) {
2134
+ parsed.watch = true;
2135
+ } else if (arg === "--verbose" || arg === "-v") {
2136
+ parsed.verbose = true;
2137
+ } else if ((arg === "--timeout" || arg === "-t") && nextArg) {
2138
+ parsed.timeout = parseInt(nextArg, 10);
2139
+ i++;
2140
+ } else if (!arg.startsWith("-") && !parsed.name) {
2141
+ // Accept positional argument as name
2142
+ parsed.name = arg;
2143
+ }
2144
+ }
2145
+
2146
+ return parsed;
2147
+ }
2148
+
2149
+ /**
2150
+ * Handle the status command - show status of a serverless function
2151
+ */
2152
+ async function handleStatus(args = []) {
2153
+ try {
2154
+ const parsedArgs = parseStatusArgs(args);
2155
+ let { name, watch, verbose, timeout } = parsedArgs;
2156
+
2157
+ // Enable verbose mode if requested
2158
+ if (verbose) {
2159
+ setVerboseMode(true);
2160
+ }
2161
+
2162
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2163
+
2164
+ // If name not provided, show list selector
2165
+ if (!name) {
2166
+ console.log(chalk.cyan("\nšŸ“‹ Fetching serverless functions...\n"));
2167
+
2168
+ const allServerless = await listAllServerless(
2169
+ apiUrl,
2170
+ token,
2171
+ accountId,
2172
+ session
2173
+ );
2174
+
2175
+ if (!allServerless || !Array.isArray(allServerless)) {
2176
+ console.error(
2177
+ chalk.red(
2178
+ "\nāŒ Failed to fetch serverless: Invalid response format"
2179
+ )
2180
+ );
2181
+ return;
2182
+ }
2183
+
2184
+ if (allServerless.length === 0) {
2185
+ console.log(chalk.yellow("No serverless functions found."));
2186
+ return;
2187
+ }
2188
+
2189
+ // Build choices for the list
2190
+ const choices = allServerless.map((serverless) => {
2191
+ const runtime = serverless.Config?.Runtime || "code";
2192
+ const typeIcon =
2193
+ runtime === "git"
2194
+ ? "šŸ“¦"
2195
+ : runtime === "container"
2196
+ ? "🐳"
2197
+ : "šŸ“";
2198
+ const status = serverless.Status;
2199
+ const statusColor = getStatusColor(status);
2200
+
2201
+ return {
2202
+ name: `${serverless.Config.Name} | ${typeIcon} ${runtime} | ${statusColor(status)}`,
2203
+ value: serverless,
2204
+ };
2205
+ });
2206
+
2207
+ const selected = await search({
2208
+ message: "Select a serverless function:",
2209
+ source: async (term) => {
2210
+ if (!term) return choices;
2211
+ return choices.filter((choice) =>
2212
+ choice.name.toLowerCase().includes(term.toLowerCase())
2213
+ );
2214
+ },
2215
+ });
2216
+
2217
+ if (!selected) {
2218
+ return;
2219
+ }
2220
+
2221
+ // Display status directly since we have the full object
2222
+ displayServerlessDetails(selected);
2223
+
2224
+ // If watch mode, continue polling
2225
+ if (watch) {
2226
+ name = selected.Config.Name;
2227
+ } else {
2228
+ return;
2229
+ }
2230
+ }
2231
+
2232
+ // If not in watch mode (and name was provided), just fetch and display once
2233
+ if (!watch) {
2234
+ console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
2235
+
2236
+ // First find the serverless by name to get the ID
2237
+ const result = await listAllServerless(
2238
+ apiUrl,
2239
+ token,
2240
+ accountId,
2241
+ session,
2242
+ name
1846
2243
  );
2244
+
2245
+ if (!result || !Array.isArray(result) || !result[0]) {
2246
+ console.error(
2247
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2248
+ );
2249
+ console.log(
2250
+ chalk.yellow(
2251
+ "\nUse 'boltic serverless list' to see all serverless functions."
2252
+ )
2253
+ );
2254
+ return;
2255
+ }
2256
+
2257
+ // Use pullServerless to get the full details with accurate status
2258
+ const serverlessId = result[0].ParentID || result[0].ID;
2259
+ const serverless = await pullServerless(
2260
+ apiUrl,
2261
+ token,
2262
+ accountId,
2263
+ session,
2264
+ serverlessId
2265
+ );
2266
+
2267
+ if (!serverless) {
2268
+ console.error(
2269
+ chalk.red("\nāŒ Failed to fetch serverless details")
2270
+ );
2271
+ return;
2272
+ }
2273
+
2274
+ displayServerlessDetails(serverless);
2275
+ return;
2276
+ }
2277
+
2278
+ // Watch mode - poll for status changes
2279
+ console.log(chalk.cyan(`\nšŸ‘ļø Watching status for "${name}"...\n`));
2280
+ const timeoutMsg = timeout > 0 ? ` (timeout: ${timeout}s)` : "";
2281
+ console.log(chalk.dim(`Press Ctrl+C to stop watching.${timeoutMsg}\n`));
2282
+
2283
+ // First, get the serverless ID
2284
+ const initialResult = await listAllServerless(
2285
+ apiUrl,
2286
+ token,
2287
+ accountId,
2288
+ session,
2289
+ name
2290
+ );
2291
+
2292
+ if (
2293
+ !initialResult ||
2294
+ !Array.isArray(initialResult) ||
2295
+ !initialResult[0]
2296
+ ) {
2297
+ console.error(chalk.red(`\nāŒ Serverless "${name}" not found.`));
1847
2298
  return;
1848
2299
  }
1849
2300
 
1850
- // Display status
2301
+ const serverlessId = initialResult[0].ParentID || initialResult[0].ID;
2302
+ const terminalStates = ["running", "failed", "degraded", "suspended"];
2303
+ let lastStatus = null;
2304
+ let iteration = 0;
2305
+ const startTime = Date.now();
2306
+
2307
+ while (true) {
2308
+ iteration++;
2309
+
2310
+ // Check timeout (-1 means infinite)
2311
+ if (timeout > 0) {
2312
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
2313
+ if (elapsed >= timeout) {
2314
+ console.log(
2315
+ chalk.yellow(
2316
+ `\n\nā±ļø Timeout reached after ${timeout} seconds.`
2317
+ )
2318
+ );
2319
+ return;
2320
+ }
2321
+ }
2322
+
2323
+ // Use pullServerless for accurate status
2324
+ const serverless = await pullServerless(
2325
+ apiUrl,
2326
+ token,
2327
+ accountId,
2328
+ session,
2329
+ serverlessId
2330
+ );
2331
+
2332
+ if (!serverless) {
2333
+ console.error(
2334
+ chalk.red(`\nāŒ Failed to fetch serverless status.`)
2335
+ );
2336
+ return;
2337
+ }
2338
+ const status = serverless.Status;
2339
+ const statusColor = getStatusColor(status);
2340
+ const url = getServerlessUrl(serverless);
2341
+
2342
+ // Show status update
2343
+ const timestamp = new Date().toLocaleTimeString();
2344
+ if (status !== lastStatus) {
2345
+ console.log(
2346
+ chalk.dim(`[${timestamp}]`) +
2347
+ ` Status: ${statusColor(status)}` +
2348
+ (url ? chalk.dim(` | ${url}`) : "")
2349
+ );
2350
+ lastStatus = status;
2351
+ } else if (iteration % 3 === 0) {
2352
+ // Show a dot every 3 iterations to indicate it's still polling
2353
+ process.stdout.write(chalk.dim("."));
2354
+ }
2355
+
2356
+ // Check if we've reached a terminal state
2357
+ if (terminalStates.includes(status)) {
2358
+ console.log();
2359
+ displayServerlessDetails(serverless);
2360
+ console.log(
2361
+ chalk.green(`\nāœ“ Reached terminal state: ${status}`)
2362
+ );
2363
+ return;
2364
+ }
2365
+
2366
+ // Wait before next poll
2367
+ await new Promise((resolve) => setTimeout(resolve, 5000));
2368
+ }
2369
+ } catch (error) {
2370
+ if (
2371
+ error.message &&
2372
+ error.message.includes("User force closed the prompt")
2373
+ ) {
2374
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
2375
+ return;
2376
+ }
2377
+ console.error(
2378
+ chalk.red("\nāŒ An error occurred:"),
2379
+ error.message || "Unknown error"
2380
+ );
2381
+ }
2382
+ }
2383
+
2384
+ /**
2385
+ * Helper to select a serverless function interactively
2386
+ */
2387
+ async function selectServerless(
2388
+ apiUrl,
2389
+ token,
2390
+ accountId,
2391
+ session,
2392
+ message = "Select a serverless function:"
2393
+ ) {
2394
+ const allServerless = await listAllServerless(
2395
+ apiUrl,
2396
+ token,
2397
+ accountId,
2398
+ session
2399
+ );
2400
+
2401
+ if (!allServerless || !Array.isArray(allServerless)) {
2402
+ throw new Error("Failed to fetch serverless: Invalid response format");
2403
+ }
2404
+
2405
+ if (allServerless.length === 0) {
2406
+ console.log(chalk.yellow("No serverless functions found."));
2407
+ return null;
2408
+ }
2409
+
2410
+ const choices = allServerless.map((serverless) => {
1851
2411
  const runtime = serverless.Config?.Runtime || "code";
1852
2412
  const typeIcon =
1853
2413
  runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
1854
2414
  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
2415
 
1864
- console.log(chalk.cyan("━".repeat(60)));
1865
- console.log(chalk.bold("\nšŸ“Š Serverless Status\n"));
2416
+ return {
2417
+ name: `${serverless.Config.Name} | ${typeIcon} ${runtime} | ${status}`,
2418
+ value: serverless,
2419
+ };
2420
+ });
2421
+
2422
+ return await search({
2423
+ message,
2424
+ source: async (term) => {
2425
+ if (!term) return choices;
2426
+ return choices.filter((choice) =>
2427
+ choice.name.toLowerCase().includes(term.toLowerCase())
2428
+ );
2429
+ },
2430
+ });
2431
+ }
2432
+
2433
+ /**
2434
+ * Handle the builds command - list builds for a serverless function
2435
+ */
2436
+ async function handleBuilds(args = []) {
2437
+ try {
2438
+ // Parse name from args (supports --name, -n, or positional)
2439
+ let name = null;
2440
+ for (let i = 0; i < args.length; i++) {
2441
+ const arg = args[i];
2442
+ if ((arg === "--name" || arg === "-n") && args[i + 1]) {
2443
+ name = args[i + 1];
2444
+ break;
2445
+ } else if (!arg.startsWith("-") && !name) {
2446
+ name = arg;
2447
+ }
2448
+ }
2449
+
2450
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2451
+
2452
+ let serverless;
2453
+
2454
+ // If name not provided, show selector
2455
+ if (!name) {
2456
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2457
+ serverless = await selectServerless(
2458
+ apiUrl,
2459
+ token,
2460
+ accountId,
2461
+ session,
2462
+ "Select serverless to view builds:"
2463
+ );
2464
+
2465
+ if (!serverless) {
2466
+ return;
2467
+ }
2468
+ } else {
2469
+ // Fetch by name
2470
+ const result = await listAllServerless(
2471
+ apiUrl,
2472
+ token,
2473
+ accountId,
2474
+ session,
2475
+ name
2476
+ );
2477
+
2478
+ if (!result || !Array.isArray(result) || !result[0]) {
2479
+ console.error(
2480
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2481
+ );
2482
+ return;
2483
+ }
2484
+ serverless = result[0];
2485
+ }
2486
+
2487
+ // Check if serverless is container type - builds are not available
2488
+ const runtime = serverless.Config?.Runtime || "code";
2489
+ if (runtime === "container") {
2490
+ console.log(
2491
+ chalk.yellow(
2492
+ `\nāš ļø Builds are not available for container-type serverless functions.`
2493
+ )
2494
+ );
2495
+ console.log(
2496
+ chalk.dim(
2497
+ ` Container images are built externally and pulled directly.`
2498
+ )
2499
+ );
2500
+ console.log(
2501
+ chalk.dim(
2502
+ `\n To view runtime logs, use: boltic serverless logs ${serverless.Config.Name}`
2503
+ )
2504
+ );
2505
+ return;
2506
+ }
2507
+
1866
2508
  console.log(
1867
- chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
2509
+ chalk.cyan(
2510
+ `\nšŸ”Ø Fetching builds for "${serverless.Config.Name}"...\n`
2511
+ )
1868
2512
  );
1869
- console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
2513
+
2514
+ const buildsData = await getServerlessBuilds(
2515
+ apiUrl,
2516
+ token,
2517
+ accountId,
2518
+ session,
2519
+ serverless.ID
2520
+ );
2521
+
2522
+ if (!buildsData || !buildsData.data || buildsData.data.length === 0) {
2523
+ console.log(chalk.yellow("No builds found for this serverless."));
2524
+ return;
2525
+ }
2526
+
2527
+ const builds = buildsData.data;
2528
+
2529
+ console.log(chalk.green(`Found ${builds.length} build(s):\n`));
2530
+ console.log(chalk.cyan("━".repeat(100)));
1870
2531
  console.log(
1871
- chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
2532
+ chalk.bold(" # ") +
2533
+ chalk.bold("Status".padEnd(12)) +
2534
+ chalk.bold("Version".padEnd(10)) +
2535
+ chalk.bold("Created".padEnd(22)) +
2536
+ chalk.bold("Build ID")
1872
2537
  );
1873
- console.log(chalk.cyan(" Status: ") + statusColor(status));
2538
+ console.log(chalk.cyan("━".repeat(100)));
2539
+
2540
+ builds.forEach((build, index) => {
2541
+ const status =
2542
+ build.StatusHistory?.slice(-1)[0]?.Status ||
2543
+ build.Status ||
2544
+ "unknown";
2545
+ const statusColor = getStatusColor(status);
2546
+ const createdAt = build.CreatedAt
2547
+ ? new Date(build.CreatedAt).toLocaleString()
2548
+ : "N/A";
2549
+ const version = build.Version || "N/A";
1874
2550
 
1875
- if (serverless.Config?.CodeOpts?.Language) {
1876
2551
  console.log(
1877
- chalk.cyan(" Language: ") +
1878
- chalk.white(serverless.Config.CodeOpts.Language)
2552
+ chalk.dim(` ${String(index + 1).padStart(2)} `) +
2553
+ statusColor(status.padEnd(12)) +
2554
+ `v${String(version).padEnd(9)}` +
2555
+ createdAt.padEnd(22) +
2556
+ build.ID
1879
2557
  );
2558
+
2559
+ // Show status history for recent builds (first 3)
2560
+ if (
2561
+ index < 3 &&
2562
+ build.StatusHistory &&
2563
+ build.StatusHistory.length > 1
2564
+ ) {
2565
+ const history = build.StatusHistory.map((h) => {
2566
+ const ts = h.Timestamp
2567
+ ? new Date(h.Timestamp).toLocaleTimeString()
2568
+ : "";
2569
+ return `${h.Status}${ts ? ` (${ts})` : ""}`;
2570
+ }).join(" → ");
2571
+ console.log(chalk.dim(` └─ ${history}`));
2572
+ }
2573
+ });
2574
+
2575
+ console.log(chalk.cyan("━".repeat(100)));
2576
+ console.log(
2577
+ chalk.dim(
2578
+ "\nTip: Use 'boltic serverless build logs -n <name>' to view logs for a build."
2579
+ )
2580
+ );
2581
+ } catch (error) {
2582
+ if (
2583
+ error.message &&
2584
+ error.message.includes("User force closed the prompt")
2585
+ ) {
2586
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
2587
+ return;
1880
2588
  }
1881
- if (serverless.Config?.ContainerOpts?.Image) {
1882
- console.log(
1883
- chalk.cyan(" Image: ") +
1884
- chalk.white(serverless.Config.ContainerOpts.Image)
2589
+ console.error(
2590
+ chalk.red("\nāŒ An error occurred:"),
2591
+ error.message || "Unknown error"
2592
+ );
2593
+ }
2594
+ }
2595
+
2596
+ /**
2597
+ * Handle the logs command - show logs for a serverless function
2598
+ */
2599
+ async function handleLogs(args = []) {
2600
+ try {
2601
+ // Parse args (supports --name, -n, or positional)
2602
+ let name = null;
2603
+ let follow = false;
2604
+ let lines = 100;
2605
+
2606
+ for (let i = 0; i < args.length; i++) {
2607
+ const arg = args[i];
2608
+ const nextArg = args[i + 1];
2609
+
2610
+ if ((arg === "--name" || arg === "-n") && nextArg) {
2611
+ name = nextArg;
2612
+ i++;
2613
+ } else if (arg === "--follow" || arg === "-f") {
2614
+ follow = true;
2615
+ } else if ((arg === "--lines" || arg === "-l") && nextArg) {
2616
+ lines = parseInt(nextArg, 10) || 100;
2617
+ i++;
2618
+ } else if (!arg.startsWith("-") && !name) {
2619
+ // Accept positional argument as name
2620
+ name = arg;
2621
+ }
2622
+ }
2623
+
2624
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2625
+
2626
+ let serverless;
2627
+
2628
+ // If name not provided, show selector
2629
+ if (!name) {
2630
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2631
+ serverless = await selectServerless(
2632
+ apiUrl,
2633
+ token,
2634
+ accountId,
2635
+ session,
2636
+ "Select serverless to view logs:"
2637
+ );
2638
+
2639
+ if (!serverless) {
2640
+ return;
2641
+ }
2642
+ } else {
2643
+ // Fetch by name
2644
+ const result = await listAllServerless(
2645
+ apiUrl,
2646
+ token,
2647
+ accountId,
2648
+ session,
2649
+ name
1885
2650
  );
2651
+
2652
+ if (!result || !Array.isArray(result) || !result[0]) {
2653
+ console.error(
2654
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2655
+ );
2656
+ return;
2657
+ }
2658
+ serverless = result[0];
1886
2659
  }
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
- )
2660
+
2661
+ console.log(
2662
+ chalk.cyan(
2663
+ `\nšŸ“œ Fetching logs for "${serverless.Config.Name}"...\n`
2664
+ )
2665
+ );
2666
+
2667
+ if (follow) {
2668
+ console.log(chalk.dim("Following logs... Press Ctrl+C to stop.\n"));
2669
+ }
2670
+
2671
+ // Track seen log IDs to avoid duplicates in follow mode
2672
+ const seenLogIds = new Set();
2673
+
2674
+ const fetchAndDisplayLogs = async (afterTimestamp = null) => {
2675
+ const now = Math.floor(Date.now() / 1000);
2676
+ const logsData = await getServerlessLogs(
2677
+ apiUrl,
2678
+ token,
2679
+ accountId,
2680
+ session,
2681
+ serverless.ID,
2682
+ {
2683
+ limit: lines,
2684
+ // For follow mode: fetch logs AFTER the last seen timestamp
2685
+ // For initial fetch: get last 24 hours
2686
+ timestampStart: afterTimestamp || now - 24 * 60 * 60,
2687
+ timestampEnd: now,
2688
+ }
1893
2689
  );
2690
+
2691
+ if (!logsData || !logsData.data || logsData.data.length === 0) {
2692
+ if (!follow && !afterTimestamp) {
2693
+ console.log(
2694
+ chalk.yellow("No logs found for this serverless.")
2695
+ );
2696
+ }
2697
+ return afterTimestamp;
2698
+ }
2699
+
2700
+ const logs = logsData.data;
2701
+ let latestTimestamp = afterTimestamp;
2702
+
2703
+ // Sort logs by timestamp ascending for proper display order
2704
+ const sortedLogs = [...logs].sort(
2705
+ (a, b) => (a.Timestamp || 0) - (b.Timestamp || 0)
2706
+ );
2707
+
2708
+ sortedLogs.forEach((log) => {
2709
+ // Create a unique ID for deduplication
2710
+ const logId = `${log.Timestamp}-${log.Log}`;
2711
+ if (seenLogIds.has(logId)) {
2712
+ return; // Skip duplicate
2713
+ }
2714
+ seenLogIds.add(logId);
2715
+
2716
+ // Timestamp is unix epoch in seconds
2717
+ const timestamp = log.Timestamp
2718
+ ? new Date(log.Timestamp * 1000).toLocaleTimeString()
2719
+ : "";
2720
+ const severity = log.Severity || "INFO";
2721
+ const severityColor =
2722
+ severity === "ERROR"
2723
+ ? chalk.red
2724
+ : severity === "WARNING" || severity === "WARN"
2725
+ ? chalk.yellow
2726
+ : severity === "DEBUG"
2727
+ ? chalk.blue
2728
+ : chalk.gray;
2729
+
2730
+ // Parse the Log field which may contain JSON
2731
+ let message = "";
2732
+ if (log.Log) {
2733
+ try {
2734
+ const parsed = JSON.parse(log.Log);
2735
+ message = parsed.msg || parsed.message || log.Log;
2736
+ } catch {
2737
+ // Not JSON, use as-is
2738
+ message = log.Log;
2739
+ }
2740
+ }
2741
+
2742
+ console.log(
2743
+ chalk.dim(`[${timestamp}]`) +
2744
+ ` ${severityColor(severity.padEnd(7))} ${message}`
2745
+ );
2746
+
2747
+ if (
2748
+ log.Timestamp &&
2749
+ (!latestTimestamp || log.Timestamp > latestTimestamp)
2750
+ ) {
2751
+ latestTimestamp = log.Timestamp;
2752
+ }
2753
+ });
2754
+
2755
+ return latestTimestamp;
2756
+ };
2757
+
2758
+ let lastTimestamp = await fetchAndDisplayLogs();
2759
+
2760
+ if (follow) {
2761
+ // Poll for new logs every 2 seconds
2762
+ while (true) {
2763
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2764
+ lastTimestamp = await fetchAndDisplayLogs(lastTimestamp);
2765
+ }
1894
2766
  }
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
- )
2767
+ } catch (error) {
2768
+ if (
2769
+ error.message &&
2770
+ error.message.includes("User force closed the prompt")
2771
+ ) {
2772
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
2773
+ return;
2774
+ }
2775
+ console.error(
2776
+ chalk.red("\nāŒ An error occurred:"),
2777
+ error.message || "Unknown error"
2778
+ );
2779
+ }
2780
+ }
2781
+
2782
+ /**
2783
+ * Handle the "build logs" command - show logs for a specific build
2784
+ */
2785
+ async function handleBuildLogs(args = []) {
2786
+ try {
2787
+ // Parse args (supports --name, -n, --build, -b, --follow, -f)
2788
+ let name = null;
2789
+ let buildId = null;
2790
+ let follow = false;
2791
+
2792
+ for (let i = 0; i < args.length; i++) {
2793
+ const arg = args[i];
2794
+ const nextArg = args[i + 1];
2795
+
2796
+ if ((arg === "--name" || arg === "-n") && nextArg) {
2797
+ name = nextArg;
2798
+ i++;
2799
+ } else if ((arg === "--build" || arg === "-b") && nextArg) {
2800
+ buildId = nextArg;
2801
+ i++;
2802
+ } else if (arg === "--follow" || arg === "-f") {
2803
+ follow = true;
2804
+ } else if (!arg.startsWith("-") && !name) {
2805
+ // Accept positional argument as name
2806
+ name = arg;
2807
+ }
2808
+ }
2809
+
2810
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2811
+
2812
+ let serverless;
2813
+
2814
+ // If name not provided, show selector
2815
+ if (!name) {
2816
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2817
+ serverless = await selectServerless(
2818
+ apiUrl,
2819
+ token,
2820
+ accountId,
2821
+ session,
2822
+ "Select serverless to view build logs:"
1901
2823
  );
2824
+
2825
+ if (!serverless) {
2826
+ return;
2827
+ }
2828
+ } else {
2829
+ // Fetch by name
2830
+ const result = await listAllServerless(
2831
+ apiUrl,
2832
+ token,
2833
+ accountId,
2834
+ session,
2835
+ name
2836
+ );
2837
+
2838
+ if (!result || !Array.isArray(result) || !result[0]) {
2839
+ console.error(
2840
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2841
+ );
2842
+ return;
2843
+ }
2844
+ serverless = result[0];
1902
2845
  }
1903
- if (serverless.RegionID) {
2846
+
2847
+ // Check if serverless is container type - build logs are not available
2848
+ const runtime = serverless.Config?.Runtime || "code";
2849
+ if (runtime === "container") {
2850
+ console.log(
2851
+ chalk.yellow(
2852
+ `\nāš ļø Build logs are not available for container-type serverless functions.`
2853
+ )
2854
+ );
1904
2855
  console.log(
1905
- chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
2856
+ chalk.dim(
2857
+ ` Container images are built externally and pulled directly.`
2858
+ )
1906
2859
  );
2860
+ console.log(
2861
+ chalk.dim(
2862
+ `\n To view runtime logs, use: boltic serverless logs ${serverless.Config.Name}`
2863
+ )
2864
+ );
2865
+ return;
1907
2866
  }
1908
- if (serverless.CreatedAt) {
2867
+
2868
+ // If build ID not provided, fetch builds and let user select
2869
+ if (!buildId) {
1909
2870
  console.log(
1910
- chalk.cyan(" Created: ") +
1911
- chalk.white(new Date(serverless.CreatedAt).toLocaleString())
2871
+ chalk.cyan(
2872
+ `\nšŸ”Ø Fetching builds for "${serverless.Config.Name}"...\n`
2873
+ )
1912
2874
  );
2875
+
2876
+ const buildsData = await getServerlessBuilds(
2877
+ apiUrl,
2878
+ token,
2879
+ accountId,
2880
+ session,
2881
+ serverless.ID
2882
+ );
2883
+
2884
+ if (
2885
+ !buildsData ||
2886
+ !buildsData.data ||
2887
+ buildsData.data.length === 0
2888
+ ) {
2889
+ console.log(
2890
+ chalk.yellow("No builds found for this serverless.")
2891
+ );
2892
+ return;
2893
+ }
2894
+
2895
+ const builds = buildsData.data;
2896
+
2897
+ const buildChoices = builds.map((build, index) => {
2898
+ const status =
2899
+ build.StatusHistory?.slice(-1)[0]?.Status ||
2900
+ build.Status ||
2901
+ "unknown";
2902
+ const statusColor = getStatusColor(status);
2903
+ const createdAt = build.CreatedAt
2904
+ ? new Date(build.CreatedAt).toLocaleString()
2905
+ : "N/A";
2906
+
2907
+ return {
2908
+ name: `#${index + 1} | ${statusColor(status)} | ${createdAt} | ${build.ID.substring(0, 8)}...`,
2909
+ value: build,
2910
+ };
2911
+ });
2912
+
2913
+ const selectedBuild = await search({
2914
+ message: "Select a build to view logs:",
2915
+ source: async (term) => {
2916
+ if (!term) return buildChoices;
2917
+ return buildChoices.filter((choice) =>
2918
+ choice.name.toLowerCase().includes(term.toLowerCase())
2919
+ );
2920
+ },
2921
+ });
2922
+
2923
+ if (!selectedBuild) {
2924
+ return;
2925
+ }
2926
+
2927
+ buildId = selectedBuild.ID;
1913
2928
  }
1914
- if (serverless.UpdatedAt) {
2929
+
2930
+ console.log(chalk.cyan(`\nšŸ“œ Fetching build logs...\n`));
2931
+
2932
+ if (follow) {
1915
2933
  console.log(
1916
- chalk.cyan(" Updated: ") +
1917
- chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
2934
+ chalk.dim("Following build logs... Press Ctrl+C to stop.\n")
1918
2935
  );
1919
2936
  }
1920
2937
 
1921
- console.log();
1922
- console.log(chalk.cyan("━".repeat(60)));
2938
+ console.log(chalk.cyan("━".repeat(80)));
2939
+ console.log(chalk.bold("Build Logs:\n"));
2940
+
2941
+ // Track displayed lines to avoid duplicates in follow mode
2942
+ let displayedLines = 0;
2943
+
2944
+ const fetchAndDisplayBuildLogs = async () => {
2945
+ const logsData = await getBuildLogs(
2946
+ apiUrl,
2947
+ token,
2948
+ accountId,
2949
+ session,
2950
+ serverless.ID,
2951
+ buildId
2952
+ );
2953
+
2954
+ if (!logsData || !logsData.data) {
2955
+ if (!follow && displayedLines === 0) {
2956
+ console.log(chalk.yellow("No logs found for this build."));
2957
+ }
2958
+ return { hasLogs: false, buildComplete: false };
2959
+ }
2960
+
2961
+ // Handle different log formats
2962
+ const logs = Array.isArray(logsData.data)
2963
+ ? logsData.data
2964
+ : [logsData.data];
2965
+
2966
+ // Only display new logs (skip already displayed ones)
2967
+ const newLogs = logs.slice(displayedLines);
2968
+
2969
+ newLogs.forEach((log) => {
2970
+ if (typeof log === "string") {
2971
+ console.log(log);
2972
+ } else if (log.Log) {
2973
+ // Log field contains the actual log content (may include ANSI colors)
2974
+ // Output directly to preserve color codes
2975
+ process.stdout.write(log.Log);
2976
+ if (!log.Log.endsWith("\n")) {
2977
+ process.stdout.write("\n");
2978
+ }
2979
+ } else if (log.Message || log.message) {
2980
+ const timestamp = log.Timestamp
2981
+ ? new Date(log.Timestamp * 1000).toLocaleTimeString()
2982
+ : "";
2983
+ console.log(
2984
+ chalk.dim(`[${timestamp}]`) +
2985
+ ` ${log.Message || log.message}`
2986
+ );
2987
+ } else {
2988
+ console.log(JSON.stringify(log, null, 2));
2989
+ }
2990
+ });
2991
+
2992
+ displayedLines = logs.length;
2993
+
2994
+ // Check if build is complete by looking for completion indicators
2995
+ const lastLog = logs[logs.length - 1];
2996
+ const logContent =
2997
+ typeof lastLog === "string"
2998
+ ? lastLog
2999
+ : lastLog?.Log || lastLog?.Message || "";
3000
+ const buildComplete =
3001
+ logContent.includes("Build completed") ||
3002
+ logContent.includes("Build failed") ||
3003
+ logContent.includes("successfully") ||
3004
+ logContent.includes("error:");
3005
+
3006
+ return { hasLogs: true, buildComplete };
3007
+ };
3008
+
3009
+ let result = await fetchAndDisplayBuildLogs();
3010
+
3011
+ if (follow && !result.buildComplete) {
3012
+ // Poll for new logs every 2 seconds until build completes
3013
+ while (true) {
3014
+ await new Promise((resolve) => setTimeout(resolve, 2000));
3015
+ result = await fetchAndDisplayBuildLogs();
3016
+ if (result.buildComplete) {
3017
+ console.log(
3018
+ chalk.dim("\n\nBuild completed. Stopping log follow.")
3019
+ );
3020
+ break;
3021
+ }
3022
+ }
3023
+ }
3024
+
3025
+ console.log("\n" + chalk.cyan("━".repeat(80)));
1923
3026
  } catch (error) {
1924
3027
  if (
1925
3028
  error.message &&