@boltic/cli 1.0.40 → 1.0.42

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.
@@ -38,7 +38,11 @@ import {
38
38
  pullServerless,
39
39
  publishServerless,
40
40
  updateServerless,
41
+ getServerlessBuilds,
42
+ getServerlessLogs,
43
+ getBuildLogs,
41
44
  } from "../api/serverless.js";
45
+ import { setVerboseMode } from "../helper/verbose.js";
42
46
 
43
47
  // Define commands and their descriptions
44
48
  const commands = {
@@ -58,10 +62,6 @@ const commands = {
58
62
  description: "Test a serverless function locally",
59
63
  action: handleTest,
60
64
  },
61
- help: {
62
- description: "Show help for serverless commands",
63
- action: showHelp,
64
- },
65
65
  list: {
66
66
  description: "List all serverless functions",
67
67
  action: handleList,
@@ -70,6 +70,22 @@ const commands = {
70
70
  description: "Show status of a serverless function",
71
71
  action: handleStatus,
72
72
  },
73
+ builds: {
74
+ description: "List builds for a serverless function",
75
+ action: handleBuilds,
76
+ },
77
+ logs: {
78
+ description: "Show logs for a serverless function",
79
+ action: handleLogs,
80
+ },
81
+ "build logs": {
82
+ description: "Show logs for a specific build",
83
+ action: handleBuildLogs,
84
+ },
85
+ help: {
86
+ description: "Show help for serverless commands",
87
+ action: showHelp,
88
+ },
73
89
  };
74
90
 
75
91
  // Serverless type choices for dropdown
@@ -381,7 +397,7 @@ async function handleCodeTypeCreate(name, language, version, targetDir) {
381
397
  }
382
398
 
383
399
  /**
384
- * Handle git type serverless creation - creates folder with boltic.yaml and calls create API
400
+ * Handle git type serverless creation - creates serverless on server and clones the repo
385
401
  */
386
402
  async function handleGitTypeCreate(name, language, version, targetDir) {
387
403
  console.log(chalk.cyan("\nšŸ“ Creating git-based serverless project..."));
@@ -442,7 +458,6 @@ async function handleGitTypeCreate(name, language, version, targetDir) {
442
458
  }
443
459
 
444
460
  // Extract serverless ID and git info from response
445
- // Response structure: { ID, Links: { Git: { Repository: { SshURL, HtmlURL, CloneURL, ... } } } }
446
461
  const serverlessId = response.ID || response.data?.ID || response._id;
447
462
  const gitRepo =
448
463
  response.Links?.Git?.Repository ||
@@ -451,8 +466,41 @@ async function handleGitTypeCreate(name, language, version, targetDir) {
451
466
  const gitHttpUrl = gitRepo?.HtmlURL || "";
452
467
  const gitCloneUrl = gitRepo?.CloneURL || "";
453
468
 
454
- // Create boltic.yaml with serverlessId inside serverlessConfig
455
- const bolticYamlContent = `app: "${name}"
469
+ // Remove the empty directory created earlier - we'll clone into it
470
+ try {
471
+ fs.rmSync(targetDir, { recursive: true, force: true });
472
+ } catch {
473
+ // Ignore cleanup errors
474
+ }
475
+
476
+ // Clone the repo from server (which has the server-generated boltic.yaml)
477
+ let cloneSuccess = false;
478
+ if (gitSshUrl) {
479
+ console.log(chalk.cyan("\nšŸ“„ Cloning git repository..."));
480
+ try {
481
+ // Clone the repo
482
+ execSync(`git clone ${gitSshUrl} "${targetDir}"`, {
483
+ stdio: "pipe",
484
+ timeout: 30000,
485
+ });
486
+ cloneSuccess = true;
487
+ console.log(chalk.green("āœ… Repository cloned successfully!"));
488
+ } catch (err) {
489
+ console.log(
490
+ chalk.yellow(
491
+ "āš ļø Could not clone repository. You may not have SSH access yet."
492
+ )
493
+ );
494
+ cloneSuccess = false;
495
+ }
496
+ }
497
+
498
+ // If clone failed, create directory with minimal setup
499
+ if (!cloneSuccess) {
500
+ try {
501
+ fs.mkdirSync(targetDir, { recursive: true });
502
+ // Create a minimal boltic.yaml as fallback
503
+ const bolticYamlContent = `app: "${name}"
456
504
  region: "asia-south1"
457
505
  handler: "${HANDLER_MAPPING[language]}"
458
506
  language: "${language}/${version}"
@@ -460,19 +508,7 @@ language: "${language}/${version}"
460
508
  serverlessConfig:
461
509
  serverlessId: "${serverlessId}"
462
510
  Name: "${name}"
463
- Description: ""
464
511
  Runtime: "git"
465
- # Environment variables for your serverless function
466
- # To add env variables, replace {} with key-value pairs like:
467
- # Env:
468
- # API_KEY: "your-api-key"
469
- #TO add port map, replace {} with port map like:
470
- # PortMap:
471
- # - Name: "port"
472
- # Port: "8080"
473
- # Protocol: "http"/"https"
474
- Env: {}
475
- PortMap: {}
476
512
  Scaling:
477
513
  AutoStop: false
478
514
  Min: 1
@@ -483,61 +519,23 @@ serverlessConfig:
483
519
  MemoryMB: 128
484
520
  MemoryMaxMB: 128
485
521
  Timeout: 60
486
- Validations: null
487
-
488
- build:
489
- builtin: dockerfile
490
- ignorefile: .gitignore
491
522
  `;
492
-
493
- try {
494
- fs.writeFileSync(
495
- path.join(targetDir, "boltic.yaml"),
496
- bolticYamlContent
497
- );
498
- } catch (err) {
499
- console.error(chalk.red(`\nāŒ Failed to create boltic.yaml`));
500
- console.error(chalk.red(`Error: ${err.message}`));
501
- return;
502
- }
503
-
504
- // Check if user has git access by trying ls-remote
505
- let hasGitAccess = false;
506
- if (gitSshUrl) {
507
- console.log(chalk.cyan("\nšŸ” Checking git repository access..."));
508
- try {
523
+ fs.writeFileSync(
524
+ path.join(targetDir, "boltic.yaml"),
525
+ bolticYamlContent
526
+ );
509
527
  // Initialize git repo
510
- execSync(`git init`, { cwd: targetDir, stdio: "pipe" });
511
- execSync(`git remote add origin ${gitSshUrl}`, {
512
- cwd: targetDir,
513
- stdio: "pipe",
514
- });
515
- // Try ls-remote to check SSH access
516
- execSync(`git ls-remote ${gitSshUrl}`, {
517
- cwd: targetDir,
518
- stdio: "pipe",
519
- timeout: 15000,
520
- });
521
- hasGitAccess = true;
522
- } catch (err) {
523
- hasGitAccess = false;
524
- }
525
- }
526
-
527
- // If user has access, create main branch
528
- if (hasGitAccess) {
529
- try {
530
- console.log(chalk.cyan("šŸ”§ Setting up git branch..."));
531
- // Create main branch
532
- execSync(`git checkout -b main`, { cwd: targetDir, stdio: "pipe" });
533
- console.log(chalk.green("āœ“ Created main branch"));
528
+ if (gitSshUrl) {
529
+ execSync(`git init`, { cwd: targetDir, stdio: "pipe" });
530
+ execSync(`git remote add origin ${gitSshUrl}`, {
531
+ cwd: targetDir,
532
+ stdio: "pipe",
533
+ });
534
+ }
534
535
  } catch (err) {
535
- // Ignore errors in branch setup, user can do it manually
536
- console.log(
537
- chalk.yellow(
538
- "āš ļø Could not auto-setup git branch. You can set it up manually."
539
- )
540
- );
536
+ console.error(chalk.red(`\nāŒ Failed to create project directory`));
537
+ console.error(chalk.red(`Error: ${err.message}`));
538
+ return;
541
539
  }
542
540
  }
543
541
 
@@ -573,11 +571,10 @@ build:
573
571
  }
574
572
  console.log();
575
573
 
576
- if (hasGitAccess) {
574
+ if (cloneSuccess) {
577
575
  console.log(
578
- chalk.green("āœ… You have access to the git repository!")
576
+ chalk.green("āœ… Repository cloned with server configuration!")
579
577
  );
580
- console.log(chalk.green("āœ… Main branch created!"));
581
578
  console.log();
582
579
  console.log(
583
580
  chalk.yellow("šŸ“ Next steps - Add your code and push:")
@@ -585,11 +582,13 @@ build:
585
582
  console.log(chalk.dim(" 1. Add your server code to this folder"));
586
583
  console.log(chalk.dim(" 2. Commit and push:"));
587
584
  console.log(chalk.white(` git add .`));
588
- console.log(chalk.white(` git commit -m "Initial commit"`));
589
- console.log(chalk.white(` git push -u origin main`));
585
+ console.log(
586
+ chalk.white(` git commit -m "Add application code"`)
587
+ );
588
+ console.log(chalk.white(` git push origin main`));
590
589
  } else {
591
590
  console.log(
592
- chalk.red("āŒ You don't have access to this git repository.")
591
+ chalk.yellow("āš ļø Could not clone repository automatically.")
593
592
  );
594
593
  console.log(
595
594
  chalk.yellow(
@@ -598,14 +597,20 @@ build:
598
597
  );
599
598
  console.log();
600
599
  console.log(
601
- chalk.yellow("šŸ“ Once you have access, push your code:")
600
+ chalk.yellow("šŸ“ Once you have access, sync with remote:")
601
+ );
602
+ console.log(chalk.dim(" 1. Pull the server config first:"));
603
+ console.log(
604
+ chalk.white(
605
+ ` git pull origin main --allow-unrelated-histories`
606
+ )
602
607
  );
603
- console.log(chalk.dim(" 1. Add your code to this folder"));
604
- console.log(chalk.dim(" 2. Run:"));
605
- console.log(chalk.white(` git checkout -b main`));
608
+ console.log(chalk.dim(" 2. Add your code and push:"));
606
609
  console.log(chalk.white(` git add .`));
607
- console.log(chalk.white(` git commit -m "Initial commit"`));
608
- console.log(chalk.white(` git push -u origin main`));
610
+ console.log(
611
+ chalk.white(` git commit -m "Add application code"`)
612
+ );
613
+ console.log(chalk.white(` git push origin main`));
609
614
  }
610
615
  } else {
611
616
  console.log();
@@ -830,6 +835,15 @@ async function handlePublish(args = []) {
830
835
  const languageBase = parseLanguageFromConfig(language);
831
836
  const runtime = serverlessConfig?.Runtime || "code";
832
837
  let code = null;
838
+ if (runtime === "git") {
839
+ console.log(
840
+ chalk.red("\nšŸ“„ Git type serverless does not support publish")
841
+ );
842
+ console.log(
843
+ chalk.yellow("Please publish using git push origin main")
844
+ );
845
+ return;
846
+ }
833
847
 
834
848
  if (runtime === "code") {
835
849
  code = readHandlerFile(directory, languageBase, config);
@@ -1540,132 +1554,184 @@ async function handlePull(args) {
1540
1554
  function showHelp() {
1541
1555
  console.log(chalk.cyan("\nServerless Commands:\n"));
1542
1556
  Object.entries(commands).forEach(([cmd, details]) => {
1543
- console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
1557
+ console.log(chalk.bold(` ${cmd.padEnd(12)}`) + details.description);
1544
1558
  });
1545
1559
 
1546
- console.log(chalk.cyan("\nCreate Command Options:\n"));
1560
+ console.log(chalk.cyan("\nGlobal Options:\n"));
1561
+ console.log(
1562
+ chalk.bold(" --help, -h".padEnd(20)) + "Show help for a command"
1563
+ );
1564
+
1565
+ console.log(chalk.cyan("\nCreate Options:\n"));
1547
1566
  console.log(
1548
- chalk.bold(" --type, -t") +
1549
- chalk.dim(" ") +
1550
- "Serverless type: blueprint, git, or container (prompts if not provided)"
1567
+ chalk.bold(" --type, -t".padEnd(20)) +
1568
+ "Serverless type: blueprint, git, or container"
1551
1569
  );
1552
1570
  console.log(
1553
- chalk.bold(" --name, -n") +
1554
- chalk.dim(" ") +
1555
- "Name of the serverless function (required, prompts if not provided)"
1571
+ chalk.bold(" --name, -n".padEnd(20)) +
1572
+ "Name of the serverless function"
1556
1573
  );
1557
1574
  console.log(
1558
- chalk.bold(" --language, -l") +
1559
- chalk.dim(" ") +
1560
- "Programming language: nodejs, python, golang, java (prompts if not provided)"
1575
+ chalk.bold(" --language, -l".padEnd(20)) +
1576
+ "Programming language: nodejs, python, golang, java"
1561
1577
  );
1562
1578
  console.log(
1563
- chalk.bold(" --directory, -d") +
1564
- chalk.dim(" ") +
1565
- "Directory where to create the project (default: current directory)"
1579
+ chalk.bold(" --directory, -d".padEnd(20)) +
1580
+ "Directory for the project (default: current)"
1566
1581
  );
1567
1582
 
1568
- console.log(chalk.cyan("\nTest Command Options:\n"));
1583
+ console.log(chalk.cyan("\nTest Options:\n"));
1569
1584
  console.log(
1570
- chalk.bold(" --port, -p") +
1571
- chalk.dim(" ") +
1585
+ chalk.bold(" --port, -p".padEnd(20)) +
1572
1586
  "Port to run the server on (default: 8080)"
1573
1587
  );
1574
1588
  console.log(
1575
- chalk.bold(" --language, -l") +
1576
- chalk.dim(" ") +
1577
- "Language (nodejs, python, golang, java) - auto-detected if not specified"
1589
+ chalk.bold(" --language, -l".padEnd(20)) +
1590
+ "Language (auto-detected if not specified)"
1578
1591
  );
1579
1592
  console.log(
1580
- chalk.bold(" --directory, -d") +
1581
- chalk.dim(" ") +
1582
- "Base directory of the project (default: current directory)"
1593
+ chalk.bold(" --directory, -d".padEnd(20)) +
1594
+ "Base directory of the project"
1583
1595
  );
1584
1596
 
1585
- console.log(chalk.cyan("\nPublish Command Options:\n"));
1597
+ console.log(chalk.cyan("\nPublish Options:\n"));
1586
1598
  console.log(
1587
- chalk.bold(" --directory, -d") +
1588
- chalk.dim(" ") +
1589
- "Directory of the serverless project (default: current directory)"
1599
+ chalk.bold(" --directory, -d".padEnd(20)) +
1600
+ "Directory of the serverless project"
1590
1601
  );
1591
1602
 
1592
- console.log(chalk.cyan("\nStatus Command Options:\n"));
1603
+ console.log(chalk.cyan("\nStatus Options:\n"));
1604
+ console.log(
1605
+ chalk.bold(" --name, -n".padEnd(20)) +
1606
+ "Name of the serverless function"
1607
+ );
1593
1608
  console.log(
1594
- chalk.bold(" --name, -n") +
1595
- chalk.dim(" ") +
1596
- "Name of the serverless function (prompts if not provided)"
1609
+ chalk.bold(" --watch, -w".padEnd(20)) +
1610
+ "Poll until status is running, failed, or degraded"
1597
1611
  );
1598
1612
 
1599
- console.log(chalk.cyan("\nCreate Examples:\n"));
1613
+ console.log(chalk.cyan("\nBuilds Options:\n"));
1600
1614
  console.log(
1601
- chalk.dim(
1602
- " # Interactive mode (will prompt for type, name, and language)"
1603
- )
1615
+ chalk.bold(" --name, -n".padEnd(20)) +
1616
+ "Name of the serverless function"
1604
1617
  );
1605
- console.log(" boltic serverless create\n");
1606
- console.log(chalk.dim(" # Create blueprint serverless"));
1618
+
1619
+ console.log(chalk.cyan("\nLogs Options:\n"));
1607
1620
  console.log(
1608
- " boltic serverless create --type blueprint --name my-api --language nodejs\n"
1621
+ chalk.bold(" --name, -n".padEnd(20)) +
1622
+ "Name of the serverless function"
1609
1623
  );
1610
1624
  console.log(
1611
- chalk.dim(
1612
- " # Create git-based serverless (add your code, then publish)"
1613
- )
1625
+ chalk.bold(" --follow, -f".padEnd(20)) + "Follow logs in real-time"
1614
1626
  );
1615
1627
  console.log(
1616
- " boltic serverless create --type git --name my-git-func --language python\n"
1628
+ chalk.bold(" --lines, -l".padEnd(20)) +
1629
+ "Number of lines to show (default: 100)"
1630
+ );
1631
+
1632
+ console.log(chalk.cyan("\nBuild Logs Options:\n"));
1633
+ console.log(
1634
+ chalk.bold(" --name, -n".padEnd(20)) +
1635
+ "Name of the serverless function"
1617
1636
  );
1618
- console.log(chalk.dim(" # Create container-based serverless"));
1619
1637
  console.log(
1620
- " boltic serverless create --type container --name my-container --language golang\n"
1638
+ chalk.bold(" --build, -b".padEnd(20)) +
1639
+ "Build ID (prompts if not provided)"
1621
1640
  );
1622
- console.log(chalk.dim(" # With custom directory"));
1641
+
1642
+ console.log(chalk.cyan("\nExamples:\n"));
1643
+
1644
+ console.log(chalk.dim(" # Create a blueprint serverless"));
1623
1645
  console.log(
1624
- " boltic serverless create --type blueprint --name my-function --language python --directory ./projects\n"
1646
+ " boltic serverless create -t blueprint -n my-api -l nodejs\n"
1625
1647
  );
1626
1648
 
1627
- console.log(chalk.cyan("\nTest Examples:\n"));
1628
- console.log(chalk.dim(" # Basic usage - auto-detect everything"));
1629
- console.log(" boltic serverless test\n");
1630
- console.log(chalk.dim(" # Specify port"));
1631
- console.log(" boltic serverless test --port 3000\n");
1649
+ console.log(chalk.dim(" # Test locally on port 3000"));
1650
+ console.log(" boltic serverless test -p 3000\n");
1632
1651
 
1633
- console.log(chalk.cyan("\nPublish Examples:\n"));
1634
1652
  console.log(chalk.dim(" # Publish from current directory"));
1635
1653
  console.log(" boltic serverless publish\n");
1636
- console.log(chalk.dim(" # Publish from specific directory"));
1637
- console.log(" boltic serverless publish -d ./my-function\n");
1638
1654
 
1639
- console.log(chalk.cyan("\nList Examples:\n"));
1640
1655
  console.log(chalk.dim(" # List all serverless functions"));
1641
1656
  console.log(" boltic serverless list\n");
1642
1657
 
1643
- console.log(chalk.cyan("\nStatus Examples:\n"));
1644
- console.log(chalk.dim(" # Get status by name"));
1645
- console.log(" boltic serverless status -n my-function\n");
1646
- console.log(chalk.dim(" # Interactive mode (will prompt for name)"));
1647
- console.log(" boltic serverless status\n");
1658
+ console.log(chalk.dim(" # Check status with polling"));
1659
+ console.log(" boltic serverless status -n my-function --watch\n");
1660
+
1661
+ console.log(chalk.dim(" # View builds for a serverless"));
1662
+ console.log(" boltic serverless builds -n my-function\n");
1663
+
1664
+ console.log(chalk.dim(" # View runtime logs"));
1665
+ console.log(" boltic serverless logs -n my-function -f\n");
1666
+
1667
+ console.log(chalk.dim(" # View build logs"));
1668
+ console.log(" boltic serverless build logs -n my-function\n");
1648
1669
  }
1649
1670
 
1650
1671
  // Execute the serverless command
1651
1672
  const execute = async (args) => {
1652
- const subCommand = args[0];
1653
-
1654
- if (!subCommand) {
1673
+ let subCommand = args[0];
1674
+ let argsToPass = args.slice(1);
1675
+
1676
+ // Handle help flags
1677
+ if (
1678
+ !subCommand ||
1679
+ subCommand === "--help" ||
1680
+ subCommand === "-h" ||
1681
+ args.includes("--help") ||
1682
+ args.includes("-h")
1683
+ ) {
1655
1684
  showHelp();
1656
1685
  return;
1657
1686
  }
1658
1687
 
1688
+ // Handle two-word commands like "build logs"
1689
+ if (subCommand === "build" && args[1] === "logs") {
1690
+ subCommand = "build logs";
1691
+ argsToPass = args.slice(2);
1692
+ }
1693
+
1659
1694
  if (!commands[subCommand]) {
1660
- console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
1695
+ console.log(chalk.red(`Unknown serverless command: "${subCommand}"\n`));
1661
1696
  showHelp();
1662
1697
  return;
1663
1698
  }
1664
1699
 
1665
1700
  const commandObj = commands[subCommand];
1666
- await commandObj.action(args.slice(1));
1701
+ await commandObj.action(argsToPass);
1667
1702
  };
1668
1703
 
1704
+ /**
1705
+ * Get the URL for a serverless function
1706
+ */
1707
+ function getServerlessUrl(serverless) {
1708
+ const appDomain = serverless.AppDomain?.[0];
1709
+ if (appDomain) {
1710
+ return `https://${appDomain.DomainName}.${appDomain.BaseUrl || "serverless.boltic.app"}`;
1711
+ }
1712
+ return null;
1713
+ }
1714
+
1715
+ /**
1716
+ * Get status color for display
1717
+ */
1718
+ function getStatusColor(status) {
1719
+ switch (status) {
1720
+ case "running":
1721
+ return chalk.green;
1722
+ case "draft":
1723
+ case "building":
1724
+ case "pending":
1725
+ return chalk.yellow;
1726
+ case "stopped":
1727
+ case "failed":
1728
+ case "degraded":
1729
+ return chalk.red;
1730
+ default:
1731
+ return chalk.gray;
1732
+ }
1733
+ }
1734
+
1669
1735
  async function handleList(args = []) {
1670
1736
  try {
1671
1737
  const { apiUrl, token, accountId, session } = await getCurrentEnv();
@@ -1711,9 +1777,10 @@ async function handleList(args = []) {
1711
1777
  : "šŸ“";
1712
1778
  const language = serverless.Config?.CodeOpts?.Language;
1713
1779
  const status = serverless.Status;
1780
+ const url = getServerlessUrl(serverless);
1714
1781
 
1715
1782
  return {
1716
- name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${status}${language ? ` | ${language}` : ""} | ID: ${serverless.ID.substring(0, 8)}...`,
1783
+ name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | ${status}${language ? ` | ${language}` : ""}${url ? ` | ${url}` : ""}`,
1717
1784
  value: serverless,
1718
1785
  };
1719
1786
  });
@@ -1731,44 +1798,7 @@ async function handleList(args = []) {
1731
1798
 
1732
1799
  // Show details of selected serverless
1733
1800
  if (selected) {
1734
- const runtime = selected.Config?.Runtime || "code";
1735
- const typeIcon =
1736
- runtime === "git"
1737
- ? "šŸ“¦"
1738
- : runtime === "container"
1739
- ? "🐳"
1740
- : "šŸ“";
1741
-
1742
- console.log("\n" + chalk.cyan("━".repeat(60)));
1743
- console.log(chalk.bold("\nšŸ“Œ Selected Serverless Details:\n"));
1744
- console.log(
1745
- chalk.cyan(" Name: ") + chalk.white(selected.Config.Name)
1746
- );
1747
- console.log(chalk.cyan(" ID: ") + chalk.white(selected.ID));
1748
- console.log(
1749
- chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1750
- );
1751
- console.log(
1752
- chalk.cyan(" Status: ") + chalk.white(selected.Status)
1753
- );
1754
- if (selected.Config?.CodeOpts?.Language) {
1755
- console.log(
1756
- chalk.cyan(" Language: ") +
1757
- chalk.white(selected.Config.CodeOpts.Language)
1758
- );
1759
- }
1760
- if (selected.Config?.ContainerOpts?.Image) {
1761
- console.log(
1762
- chalk.cyan(" Image: ") +
1763
- chalk.white(selected.Config.ContainerOpts.Image)
1764
- );
1765
- }
1766
- console.log(chalk.cyan("━".repeat(60)));
1767
- console.log(
1768
- chalk.dim(
1769
- "\nUse 'boltic serverless pull' to pull this serverless locally."
1770
- )
1771
- );
1801
+ displayServerlessDetails(selected);
1772
1802
  }
1773
1803
  } catch (error) {
1774
1804
  if (
@@ -1785,143 +1815,927 @@ async function handleList(args = []) {
1785
1815
  }
1786
1816
  }
1787
1817
 
1818
+ /**
1819
+ * Display detailed information about a serverless function
1820
+ */
1821
+ function displayServerlessDetails(serverless) {
1822
+ const runtime = serverless.Config?.Runtime || "code";
1823
+ const typeIcon =
1824
+ runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
1825
+ const status = serverless.Status;
1826
+ const statusColor = getStatusColor(status);
1827
+ const url = getServerlessUrl(serverless);
1828
+
1829
+ console.log("\n" + chalk.cyan("━".repeat(60)));
1830
+ console.log(chalk.bold("\nšŸ“Š Serverless Details\n"));
1831
+ console.log(chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name));
1832
+ console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
1833
+ console.log(
1834
+ chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
1835
+ );
1836
+ console.log(chalk.cyan(" Status: ") + statusColor(status));
1837
+
1838
+ if (url) {
1839
+ console.log(chalk.cyan(" URL: ") + chalk.white.bold(url));
1840
+ }
1841
+
1842
+ if (serverless.Config?.CodeOpts?.Language) {
1843
+ console.log(
1844
+ chalk.cyan(" Language: ") +
1845
+ chalk.white(serverless.Config.CodeOpts.Language)
1846
+ );
1847
+ }
1848
+ if (serverless.Config?.ContainerOpts?.Image) {
1849
+ console.log(
1850
+ chalk.cyan(" Image: ") +
1851
+ chalk.white(serverless.Config.ContainerOpts.Image)
1852
+ );
1853
+ }
1854
+ if (serverless.Config?.Resources) {
1855
+ console.log(
1856
+ chalk.cyan(" Resources: ") +
1857
+ chalk.white(
1858
+ `CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
1859
+ )
1860
+ );
1861
+ }
1862
+ if (serverless.Config?.Scaling) {
1863
+ console.log(
1864
+ chalk.cyan(" Scaling: ") +
1865
+ chalk.white(
1866
+ `Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
1867
+ )
1868
+ );
1869
+ }
1870
+ if (serverless.RegionID) {
1871
+ console.log(
1872
+ chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
1873
+ );
1874
+ }
1875
+ if (serverless.CreatedAt) {
1876
+ console.log(
1877
+ chalk.cyan(" Created: ") +
1878
+ chalk.white(new Date(serverless.CreatedAt).toLocaleString())
1879
+ );
1880
+ }
1881
+ if (serverless.UpdatedAt) {
1882
+ console.log(
1883
+ chalk.cyan(" Updated: ") +
1884
+ chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
1885
+ );
1886
+ }
1887
+
1888
+ console.log();
1889
+ console.log(chalk.cyan("━".repeat(60)));
1890
+ console.log(
1891
+ chalk.dim(
1892
+ "\nTip: Use 'boltic serverless status -n <name> --watch' to poll for status changes."
1893
+ )
1894
+ );
1895
+ }
1896
+
1897
+ /**
1898
+ * Parse status command arguments
1899
+ */
1900
+ function parseStatusArgs(args) {
1901
+ const parsed = {
1902
+ name: null,
1903
+ watch: false,
1904
+ verbose: false,
1905
+ timeout: -1, // -1 means infinite
1906
+ };
1907
+
1908
+ for (let i = 0; i < args.length; i++) {
1909
+ const arg = args[i];
1910
+ const nextArg = args[i + 1];
1911
+
1912
+ if ((arg === "--name" || arg === "-n") && nextArg) {
1913
+ parsed.name = nextArg;
1914
+ i++;
1915
+ } else if (arg === "--watch" || arg === "-w") {
1916
+ parsed.watch = true;
1917
+ } else if (arg === "--verbose" || arg === "-v") {
1918
+ parsed.verbose = true;
1919
+ } else if ((arg === "--timeout" || arg === "-t") && nextArg) {
1920
+ parsed.timeout = parseInt(nextArg, 10);
1921
+ i++;
1922
+ } else if (!arg.startsWith("-") && !parsed.name) {
1923
+ // Accept positional argument as name
1924
+ parsed.name = arg;
1925
+ }
1926
+ }
1927
+
1928
+ return parsed;
1929
+ }
1930
+
1788
1931
  /**
1789
1932
  * Handle the status command - show status of a serverless function
1790
1933
  */
1791
1934
  async function handleStatus(args = []) {
1792
1935
  try {
1793
- // Parse name from args
1794
- let name = null;
1795
- const nameIndex = args.indexOf("--name");
1796
- const shortNameIndex = args.indexOf("-n");
1936
+ const parsedArgs = parseStatusArgs(args);
1937
+ let { name, watch, verbose, timeout } = parsedArgs;
1797
1938
 
1798
- if (nameIndex !== -1 && args[nameIndex + 1]) {
1799
- name = args[nameIndex + 1];
1800
- } else if (shortNameIndex !== -1 && args[shortNameIndex + 1]) {
1801
- name = args[shortNameIndex + 1];
1939
+ // Enable verbose mode if requested
1940
+ if (verbose) {
1941
+ setVerboseMode(true);
1802
1942
  }
1803
1943
 
1804
- // If name not provided, prompt for it
1944
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1945
+
1946
+ // If name not provided, show list selector
1805
1947
  if (!name) {
1806
- name = await input({
1807
- message: "Enter serverless name:",
1808
- validate: (value) => {
1809
- if (!value || value.trim() === "") {
1810
- return "Serverless name is required";
1811
- }
1812
- return true;
1948
+ console.log(chalk.cyan("\nšŸ“‹ Fetching serverless functions...\n"));
1949
+
1950
+ const allServerless = await listAllServerless(
1951
+ apiUrl,
1952
+ token,
1953
+ accountId,
1954
+ session
1955
+ );
1956
+
1957
+ if (!allServerless || !Array.isArray(allServerless)) {
1958
+ console.error(
1959
+ chalk.red(
1960
+ "\nāŒ Failed to fetch serverless: Invalid response format"
1961
+ )
1962
+ );
1963
+ return;
1964
+ }
1965
+
1966
+ if (allServerless.length === 0) {
1967
+ console.log(chalk.yellow("No serverless functions found."));
1968
+ return;
1969
+ }
1970
+
1971
+ // Build choices for the list
1972
+ const choices = allServerless.map((serverless) => {
1973
+ const runtime = serverless.Config?.Runtime || "code";
1974
+ const typeIcon =
1975
+ runtime === "git"
1976
+ ? "šŸ“¦"
1977
+ : runtime === "container"
1978
+ ? "🐳"
1979
+ : "šŸ“";
1980
+ const status = serverless.Status;
1981
+ const statusColor = getStatusColor(status);
1982
+
1983
+ return {
1984
+ name: `${serverless.Config.Name} | ${typeIcon} ${runtime} | ${statusColor(status)}`,
1985
+ value: serverless,
1986
+ };
1987
+ });
1988
+
1989
+ const selected = await search({
1990
+ message: "Select a serverless function:",
1991
+ source: async (term) => {
1992
+ if (!term) return choices;
1993
+ return choices.filter((choice) =>
1994
+ choice.name.toLowerCase().includes(term.toLowerCase())
1995
+ );
1813
1996
  },
1814
1997
  });
1998
+
1999
+ if (!selected) {
2000
+ return;
2001
+ }
2002
+
2003
+ // Display status directly since we have the full object
2004
+ displayServerlessDetails(selected);
2005
+
2006
+ // If watch mode, continue polling
2007
+ if (watch) {
2008
+ name = selected.Config.Name;
2009
+ } else {
2010
+ return;
2011
+ }
1815
2012
  }
1816
2013
 
1817
- const { apiUrl, token, accountId, session } = await getCurrentEnv();
2014
+ // If not in watch mode (and name was provided), just fetch and display once
2015
+ if (!watch) {
2016
+ console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
1818
2017
 
1819
- console.log(chalk.cyan(`\nšŸ” Fetching status for "${name}"...\n`));
2018
+ // First find the serverless by name to get the ID
2019
+ const result = await listAllServerless(
2020
+ apiUrl,
2021
+ token,
2022
+ accountId,
2023
+ session,
2024
+ name
2025
+ );
1820
2026
 
1821
- // Get serverless by name using query parameter
1822
- const result = await listAllServerless(
1823
- apiUrl,
1824
- token,
1825
- accountId,
1826
- session,
1827
- name // Pass name as query parameter
1828
- );
2027
+ if (!result || !Array.isArray(result) || !result[0]) {
2028
+ console.error(
2029
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2030
+ );
2031
+ console.log(
2032
+ chalk.yellow(
2033
+ "\nUse 'boltic serverless list' to see all serverless functions."
2034
+ )
2035
+ );
2036
+ return;
2037
+ }
1829
2038
 
1830
- if (!result || !Array.isArray(result)) {
1831
- console.error(
1832
- chalk.red(
1833
- "\nāŒ Failed to fetch serverless: Invalid response format"
1834
- )
2039
+ // Use pullServerless to get the full details with accurate status
2040
+ const serverlessId = result[0].ParentID || result[0].ID;
2041
+ const serverless = await pullServerless(
2042
+ apiUrl,
2043
+ token,
2044
+ accountId,
2045
+ session,
2046
+ serverlessId
1835
2047
  );
2048
+
2049
+ if (!serverless) {
2050
+ console.error(
2051
+ chalk.red("\nāŒ Failed to fetch serverless details")
2052
+ );
2053
+ return;
2054
+ }
2055
+
2056
+ displayServerlessDetails(serverless);
1836
2057
  return;
1837
2058
  }
1838
2059
 
1839
- // Get first element (name is unique)
1840
- const serverless = result[0];
2060
+ // Watch mode - poll for status changes
2061
+ console.log(chalk.cyan(`\nšŸ‘ļø Watching status for "${name}"...\n`));
2062
+ const timeoutMsg = timeout > 0 ? ` (timeout: ${timeout}s)` : "";
2063
+ console.log(chalk.dim(`Press Ctrl+C to stop watching.${timeoutMsg}\n`));
1841
2064
 
1842
- if (!serverless) {
2065
+ // First, get the serverless ID
2066
+ const initialResult = await listAllServerless(
2067
+ apiUrl,
2068
+ token,
2069
+ accountId,
2070
+ session,
2071
+ name
2072
+ );
2073
+
2074
+ if (
2075
+ !initialResult ||
2076
+ !Array.isArray(initialResult) ||
2077
+ !initialResult[0]
2078
+ ) {
1843
2079
  console.error(chalk.red(`\nāŒ Serverless "${name}" not found.`));
1844
- console.log(
1845
- chalk.yellow(
1846
- "\nUse 'boltic serverless list' to see all serverless functions."
1847
- )
2080
+ return;
2081
+ }
2082
+
2083
+ const serverlessId = initialResult[0].ParentID || initialResult[0].ID;
2084
+ const terminalStates = ["running", "failed", "degraded", "suspended"];
2085
+ let lastStatus = null;
2086
+ let iteration = 0;
2087
+ const startTime = Date.now();
2088
+
2089
+ while (true) {
2090
+ iteration++;
2091
+
2092
+ // Check timeout (-1 means infinite)
2093
+ if (timeout > 0) {
2094
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
2095
+ if (elapsed >= timeout) {
2096
+ console.log(
2097
+ chalk.yellow(
2098
+ `\n\nā±ļø Timeout reached after ${timeout} seconds.`
2099
+ )
2100
+ );
2101
+ return;
2102
+ }
2103
+ }
2104
+
2105
+ // Use pullServerless for accurate status
2106
+ const serverless = await pullServerless(
2107
+ apiUrl,
2108
+ token,
2109
+ accountId,
2110
+ session,
2111
+ serverlessId
1848
2112
  );
2113
+
2114
+ if (!serverless) {
2115
+ console.error(
2116
+ chalk.red(`\nāŒ Failed to fetch serverless status.`)
2117
+ );
2118
+ return;
2119
+ }
2120
+ const status = serverless.Status;
2121
+ const statusColor = getStatusColor(status);
2122
+ const url = getServerlessUrl(serverless);
2123
+
2124
+ // Show status update
2125
+ const timestamp = new Date().toLocaleTimeString();
2126
+ if (status !== lastStatus) {
2127
+ console.log(
2128
+ chalk.dim(`[${timestamp}]`) +
2129
+ ` Status: ${statusColor(status)}` +
2130
+ (url ? chalk.dim(` | ${url}`) : "")
2131
+ );
2132
+ lastStatus = status;
2133
+ } else if (iteration % 3 === 0) {
2134
+ // Show a dot every 3 iterations to indicate it's still polling
2135
+ process.stdout.write(chalk.dim("."));
2136
+ }
2137
+
2138
+ // Check if we've reached a terminal state
2139
+ if (terminalStates.includes(status)) {
2140
+ console.log();
2141
+ displayServerlessDetails(serverless);
2142
+ console.log(
2143
+ chalk.green(`\nāœ“ Reached terminal state: ${status}`)
2144
+ );
2145
+ return;
2146
+ }
2147
+
2148
+ // Wait before next poll
2149
+ await new Promise((resolve) => setTimeout(resolve, 5000));
2150
+ }
2151
+ } catch (error) {
2152
+ if (
2153
+ error.message &&
2154
+ error.message.includes("User force closed the prompt")
2155
+ ) {
2156
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
1849
2157
  return;
1850
2158
  }
2159
+ console.error(
2160
+ chalk.red("\nāŒ An error occurred:"),
2161
+ error.message || "Unknown error"
2162
+ );
2163
+ }
2164
+ }
2165
+
2166
+ /**
2167
+ * Helper to select a serverless function interactively
2168
+ */
2169
+ async function selectServerless(
2170
+ apiUrl,
2171
+ token,
2172
+ accountId,
2173
+ session,
2174
+ message = "Select a serverless function:"
2175
+ ) {
2176
+ const allServerless = await listAllServerless(
2177
+ apiUrl,
2178
+ token,
2179
+ accountId,
2180
+ session
2181
+ );
2182
+
2183
+ if (!allServerless || !Array.isArray(allServerless)) {
2184
+ throw new Error("Failed to fetch serverless: Invalid response format");
2185
+ }
2186
+
2187
+ if (allServerless.length === 0) {
2188
+ console.log(chalk.yellow("No serverless functions found."));
2189
+ return null;
2190
+ }
1851
2191
 
1852
- // Display status
2192
+ const choices = allServerless.map((serverless) => {
1853
2193
  const runtime = serverless.Config?.Runtime || "code";
1854
2194
  const typeIcon =
1855
2195
  runtime === "git" ? "šŸ“¦" : runtime === "container" ? "🐳" : "šŸ“";
1856
2196
  const status = serverless.Status;
1857
- const statusColor =
1858
- status === "running"
1859
- ? chalk.green
1860
- : status === "draft"
1861
- ? chalk.yellow
1862
- : status === "stopped"
1863
- ? chalk.red
1864
- : chalk.gray;
1865
2197
 
1866
- console.log(chalk.cyan("━".repeat(60)));
1867
- console.log(chalk.bold("\nšŸ“Š Serverless Status\n"));
2198
+ return {
2199
+ name: `${serverless.Config.Name} | ${typeIcon} ${runtime} | ${status}`,
2200
+ value: serverless,
2201
+ };
2202
+ });
2203
+
2204
+ return await search({
2205
+ message,
2206
+ source: async (term) => {
2207
+ if (!term) return choices;
2208
+ return choices.filter((choice) =>
2209
+ choice.name.toLowerCase().includes(term.toLowerCase())
2210
+ );
2211
+ },
2212
+ });
2213
+ }
2214
+
2215
+ /**
2216
+ * Handle the builds command - list builds for a serverless function
2217
+ */
2218
+ async function handleBuilds(args = []) {
2219
+ try {
2220
+ // Parse name from args (supports --name, -n, or positional)
2221
+ let name = null;
2222
+ for (let i = 0; i < args.length; i++) {
2223
+ const arg = args[i];
2224
+ if ((arg === "--name" || arg === "-n") && args[i + 1]) {
2225
+ name = args[i + 1];
2226
+ break;
2227
+ } else if (!arg.startsWith("-") && !name) {
2228
+ name = arg;
2229
+ }
2230
+ }
2231
+
2232
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2233
+
2234
+ let serverless;
2235
+
2236
+ // If name not provided, show selector
2237
+ if (!name) {
2238
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2239
+ serverless = await selectServerless(
2240
+ apiUrl,
2241
+ token,
2242
+ accountId,
2243
+ session,
2244
+ "Select serverless to view builds:"
2245
+ );
2246
+
2247
+ if (!serverless) {
2248
+ return;
2249
+ }
2250
+ } else {
2251
+ // Fetch by name
2252
+ const result = await listAllServerless(
2253
+ apiUrl,
2254
+ token,
2255
+ accountId,
2256
+ session,
2257
+ name
2258
+ );
2259
+
2260
+ if (!result || !Array.isArray(result) || !result[0]) {
2261
+ console.error(
2262
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2263
+ );
2264
+ return;
2265
+ }
2266
+ serverless = result[0];
2267
+ }
2268
+
2269
+ // Check if serverless is container type - builds are not available
2270
+ const runtime = serverless.Config?.Runtime || "code";
2271
+ if (runtime === "container") {
2272
+ console.log(
2273
+ chalk.yellow(
2274
+ `\nāš ļø Builds are not available for container-type serverless functions.`
2275
+ )
2276
+ );
2277
+ console.log(
2278
+ chalk.dim(
2279
+ ` Container images are built externally and pulled directly.`
2280
+ )
2281
+ );
2282
+ console.log(
2283
+ chalk.dim(
2284
+ `\n To view runtime logs, use: boltic serverless logs ${serverless.Config.Name}`
2285
+ )
2286
+ );
2287
+ return;
2288
+ }
2289
+
1868
2290
  console.log(
1869
- chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
2291
+ chalk.cyan(
2292
+ `\nšŸ”Ø Fetching builds for "${serverless.Config.Name}"...\n`
2293
+ )
2294
+ );
2295
+
2296
+ const buildsData = await getServerlessBuilds(
2297
+ apiUrl,
2298
+ token,
2299
+ accountId,
2300
+ session,
2301
+ serverless.ID
1870
2302
  );
1871
- console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
2303
+
2304
+ if (!buildsData || !buildsData.data || buildsData.data.length === 0) {
2305
+ console.log(chalk.yellow("No builds found for this serverless."));
2306
+ return;
2307
+ }
2308
+
2309
+ const builds = buildsData.data;
2310
+
2311
+ console.log(chalk.green(`Found ${builds.length} build(s):\n`));
2312
+ console.log(chalk.cyan("━".repeat(100)));
1872
2313
  console.log(
1873
- chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
2314
+ chalk.bold(" # ") +
2315
+ chalk.bold("Status".padEnd(12)) +
2316
+ chalk.bold("Version".padEnd(10)) +
2317
+ chalk.bold("Created".padEnd(22)) +
2318
+ chalk.bold("Build ID")
1874
2319
  );
1875
- console.log(chalk.cyan(" Status: ") + statusColor(status));
2320
+ console.log(chalk.cyan("━".repeat(100)));
2321
+
2322
+ builds.forEach((build, index) => {
2323
+ const status =
2324
+ build.StatusHistory?.slice(-1)[0]?.Status ||
2325
+ build.Status ||
2326
+ "unknown";
2327
+ const statusColor = getStatusColor(status);
2328
+ const createdAt = build.CreatedAt
2329
+ ? new Date(build.CreatedAt).toLocaleString()
2330
+ : "N/A";
2331
+ const version = build.Version || "N/A";
1876
2332
 
1877
- if (serverless.Config?.CodeOpts?.Language) {
1878
2333
  console.log(
1879
- chalk.cyan(" Language: ") +
1880
- chalk.white(serverless.Config.CodeOpts.Language)
2334
+ chalk.dim(` ${String(index + 1).padStart(2)} `) +
2335
+ statusColor(status.padEnd(12)) +
2336
+ `v${String(version).padEnd(9)}` +
2337
+ createdAt.padEnd(22) +
2338
+ build.ID
1881
2339
  );
2340
+
2341
+ // Show status history for recent builds (first 3)
2342
+ if (
2343
+ index < 3 &&
2344
+ build.StatusHistory &&
2345
+ build.StatusHistory.length > 1
2346
+ ) {
2347
+ const history = build.StatusHistory.map((h) => {
2348
+ const ts = h.Timestamp
2349
+ ? new Date(h.Timestamp).toLocaleTimeString()
2350
+ : "";
2351
+ return `${h.Status}${ts ? ` (${ts})` : ""}`;
2352
+ }).join(" → ");
2353
+ console.log(chalk.dim(` └─ ${history}`));
2354
+ }
2355
+ });
2356
+
2357
+ console.log(chalk.cyan("━".repeat(100)));
2358
+ console.log(
2359
+ chalk.dim(
2360
+ "\nTip: Use 'boltic serverless build logs -n <name>' to view logs for a build."
2361
+ )
2362
+ );
2363
+ } catch (error) {
2364
+ if (
2365
+ error.message &&
2366
+ error.message.includes("User force closed the prompt")
2367
+ ) {
2368
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
2369
+ return;
1882
2370
  }
1883
- if (serverless.Config?.ContainerOpts?.Image) {
1884
- console.log(
1885
- chalk.cyan(" Image: ") +
1886
- chalk.white(serverless.Config.ContainerOpts.Image)
2371
+ console.error(
2372
+ chalk.red("\nāŒ An error occurred:"),
2373
+ error.message || "Unknown error"
2374
+ );
2375
+ }
2376
+ }
2377
+
2378
+ /**
2379
+ * Handle the logs command - show logs for a serverless function
2380
+ */
2381
+ async function handleLogs(args = []) {
2382
+ try {
2383
+ // Parse args (supports --name, -n, or positional)
2384
+ let name = null;
2385
+ let follow = false;
2386
+ let lines = 100;
2387
+
2388
+ for (let i = 0; i < args.length; i++) {
2389
+ const arg = args[i];
2390
+ const nextArg = args[i + 1];
2391
+
2392
+ if ((arg === "--name" || arg === "-n") && nextArg) {
2393
+ name = nextArg;
2394
+ i++;
2395
+ } else if (arg === "--follow" || arg === "-f") {
2396
+ follow = true;
2397
+ } else if ((arg === "--lines" || arg === "-l") && nextArg) {
2398
+ lines = parseInt(nextArg, 10) || 100;
2399
+ i++;
2400
+ } else if (!arg.startsWith("-") && !name) {
2401
+ // Accept positional argument as name
2402
+ name = arg;
2403
+ }
2404
+ }
2405
+
2406
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2407
+
2408
+ let serverless;
2409
+
2410
+ // If name not provided, show selector
2411
+ if (!name) {
2412
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2413
+ serverless = await selectServerless(
2414
+ apiUrl,
2415
+ token,
2416
+ accountId,
2417
+ session,
2418
+ "Select serverless to view logs:"
1887
2419
  );
2420
+
2421
+ if (!serverless) {
2422
+ return;
2423
+ }
2424
+ } else {
2425
+ // Fetch by name
2426
+ const result = await listAllServerless(
2427
+ apiUrl,
2428
+ token,
2429
+ accountId,
2430
+ session,
2431
+ name
2432
+ );
2433
+
2434
+ if (!result || !Array.isArray(result) || !result[0]) {
2435
+ console.error(
2436
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2437
+ );
2438
+ return;
2439
+ }
2440
+ serverless = result[0];
1888
2441
  }
1889
- if (serverless.Config?.Resources) {
1890
- console.log(
1891
- chalk.cyan(" Resources: ") +
1892
- chalk.white(
1893
- `CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
1894
- )
2442
+
2443
+ console.log(
2444
+ chalk.cyan(
2445
+ `\nšŸ“œ Fetching logs for "${serverless.Config.Name}"...\n`
2446
+ )
2447
+ );
2448
+
2449
+ if (follow) {
2450
+ console.log(chalk.dim("Following logs... Press Ctrl+C to stop.\n"));
2451
+ }
2452
+
2453
+ const fetchAndDisplayLogs = async (timestampEnd = null) => {
2454
+ const now = Math.floor(Date.now() / 1000);
2455
+ const logsData = await getServerlessLogs(
2456
+ apiUrl,
2457
+ token,
2458
+ accountId,
2459
+ session,
2460
+ serverless.ID,
2461
+ {
2462
+ limit: lines,
2463
+ timestampEnd: timestampEnd || now,
2464
+ timestampStart: (timestampEnd || now) - 24 * 60 * 60,
2465
+ }
1895
2466
  );
2467
+
2468
+ if (!logsData || !logsData.data || logsData.data.length === 0) {
2469
+ if (!follow) {
2470
+ console.log(
2471
+ chalk.yellow("No logs found for this serverless.")
2472
+ );
2473
+ }
2474
+ return timestampEnd;
2475
+ }
2476
+
2477
+ const logs = logsData.data;
2478
+ let latestTimestamp = timestampEnd;
2479
+
2480
+ logs.forEach((log) => {
2481
+ // Timestamp is unix epoch in seconds
2482
+ const timestamp = log.Timestamp
2483
+ ? new Date(log.Timestamp * 1000).toLocaleTimeString()
2484
+ : "";
2485
+ const severity = log.Severity || "INFO";
2486
+ const severityColor =
2487
+ severity === "ERROR"
2488
+ ? chalk.red
2489
+ : severity === "WARNING" || severity === "WARN"
2490
+ ? chalk.yellow
2491
+ : severity === "DEBUG"
2492
+ ? chalk.blue
2493
+ : chalk.gray;
2494
+
2495
+ // Parse the Log field which may contain JSON
2496
+ let message = "";
2497
+ if (log.Log) {
2498
+ try {
2499
+ const parsed = JSON.parse(log.Log);
2500
+ message = parsed.msg || parsed.message || log.Log;
2501
+ } catch {
2502
+ // Not JSON, use as-is
2503
+ message = log.Log;
2504
+ }
2505
+ }
2506
+
2507
+ console.log(
2508
+ chalk.dim(`[${timestamp}]`) +
2509
+ ` ${severityColor(severity.padEnd(7))} ${message}`
2510
+ );
2511
+
2512
+ if (
2513
+ log.Timestamp &&
2514
+ (!latestTimestamp || log.Timestamp > latestTimestamp)
2515
+ ) {
2516
+ latestTimestamp = log.Timestamp;
2517
+ }
2518
+ });
2519
+
2520
+ return latestTimestamp;
2521
+ };
2522
+
2523
+ let lastTimestamp = await fetchAndDisplayLogs();
2524
+
2525
+ if (follow) {
2526
+ while (true) {
2527
+ await new Promise((resolve) => setTimeout(resolve, 3000));
2528
+ lastTimestamp = await fetchAndDisplayLogs(lastTimestamp);
2529
+ }
1896
2530
  }
1897
- if (serverless.Config?.Scaling) {
1898
- console.log(
1899
- chalk.cyan(" Scaling: ") +
1900
- chalk.white(
1901
- `Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
1902
- )
2531
+ } catch (error) {
2532
+ if (
2533
+ error.message &&
2534
+ error.message.includes("User force closed the prompt")
2535
+ ) {
2536
+ console.log(chalk.yellow("\nāš ļø Operation cancelled by user"));
2537
+ return;
2538
+ }
2539
+ console.error(
2540
+ chalk.red("\nāŒ An error occurred:"),
2541
+ error.message || "Unknown error"
2542
+ );
2543
+ }
2544
+ }
2545
+
2546
+ /**
2547
+ * Handle the "build logs" command - show logs for a specific build
2548
+ */
2549
+ async function handleBuildLogs(args = []) {
2550
+ try {
2551
+ // Parse args (supports --name, -n, or positional)
2552
+ let name = null;
2553
+ let buildId = null;
2554
+
2555
+ for (let i = 0; i < args.length; i++) {
2556
+ const arg = args[i];
2557
+ const nextArg = args[i + 1];
2558
+
2559
+ if ((arg === "--name" || arg === "-n") && nextArg) {
2560
+ name = nextArg;
2561
+ i++;
2562
+ } else if ((arg === "--build" || arg === "-b") && nextArg) {
2563
+ buildId = nextArg;
2564
+ i++;
2565
+ } else if (!arg.startsWith("-") && !name) {
2566
+ // Accept positional argument as name
2567
+ name = arg;
2568
+ }
2569
+ }
2570
+
2571
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
2572
+
2573
+ let serverless;
2574
+
2575
+ // If name not provided, show selector
2576
+ if (!name) {
2577
+ console.log(chalk.cyan("\nšŸ“‹ Select a serverless function...\n"));
2578
+ serverless = await selectServerless(
2579
+ apiUrl,
2580
+ token,
2581
+ accountId,
2582
+ session,
2583
+ "Select serverless to view build logs:"
2584
+ );
2585
+
2586
+ if (!serverless) {
2587
+ return;
2588
+ }
2589
+ } else {
2590
+ // Fetch by name
2591
+ const result = await listAllServerless(
2592
+ apiUrl,
2593
+ token,
2594
+ accountId,
2595
+ session,
2596
+ name
1903
2597
  );
2598
+
2599
+ if (!result || !Array.isArray(result) || !result[0]) {
2600
+ console.error(
2601
+ chalk.red(`\nāŒ Serverless "${name}" not found.`)
2602
+ );
2603
+ return;
2604
+ }
2605
+ serverless = result[0];
1904
2606
  }
1905
- if (serverless.RegionID) {
2607
+
2608
+ // Check if serverless is container type - build logs are not available
2609
+ const runtime = serverless.Config?.Runtime || "code";
2610
+ if (runtime === "container") {
2611
+ console.log(
2612
+ chalk.yellow(
2613
+ `\nāš ļø Build logs are not available for container-type serverless functions.`
2614
+ )
2615
+ );
1906
2616
  console.log(
1907
- chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
2617
+ chalk.dim(
2618
+ ` Container images are built externally and pulled directly.`
2619
+ )
1908
2620
  );
1909
- }
1910
- if (serverless.CreatedAt) {
1911
2621
  console.log(
1912
- chalk.cyan(" Created: ") +
1913
- chalk.white(new Date(serverless.CreatedAt).toLocaleString())
2622
+ chalk.dim(
2623
+ `\n To view runtime logs, use: boltic serverless logs ${serverless.Config.Name}`
2624
+ )
1914
2625
  );
2626
+ return;
1915
2627
  }
1916
- if (serverless.UpdatedAt) {
2628
+
2629
+ // If build ID not provided, fetch builds and let user select
2630
+ if (!buildId) {
1917
2631
  console.log(
1918
- chalk.cyan(" Updated: ") +
1919
- chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
2632
+ chalk.cyan(
2633
+ `\nšŸ”Ø Fetching builds for "${serverless.Config.Name}"...\n`
2634
+ )
2635
+ );
2636
+
2637
+ const buildsData = await getServerlessBuilds(
2638
+ apiUrl,
2639
+ token,
2640
+ accountId,
2641
+ session,
2642
+ serverless.ID
1920
2643
  );
2644
+
2645
+ if (
2646
+ !buildsData ||
2647
+ !buildsData.data ||
2648
+ buildsData.data.length === 0
2649
+ ) {
2650
+ console.log(
2651
+ chalk.yellow("No builds found for this serverless.")
2652
+ );
2653
+ return;
2654
+ }
2655
+
2656
+ const builds = buildsData.data;
2657
+
2658
+ const buildChoices = builds.map((build, index) => {
2659
+ const status =
2660
+ build.StatusHistory?.slice(-1)[0]?.Status ||
2661
+ build.Status ||
2662
+ "unknown";
2663
+ const statusColor = getStatusColor(status);
2664
+ const createdAt = build.CreatedAt
2665
+ ? new Date(build.CreatedAt).toLocaleString()
2666
+ : "N/A";
2667
+
2668
+ return {
2669
+ name: `#${index + 1} | ${statusColor(status)} | ${createdAt} | ${build.ID.substring(0, 8)}...`,
2670
+ value: build,
2671
+ };
2672
+ });
2673
+
2674
+ const selectedBuild = await search({
2675
+ message: "Select a build to view logs:",
2676
+ source: async (term) => {
2677
+ if (!term) return buildChoices;
2678
+ return buildChoices.filter((choice) =>
2679
+ choice.name.toLowerCase().includes(term.toLowerCase())
2680
+ );
2681
+ },
2682
+ });
2683
+
2684
+ if (!selectedBuild) {
2685
+ return;
2686
+ }
2687
+
2688
+ buildId = selectedBuild.ID;
1921
2689
  }
1922
2690
 
1923
- console.log();
1924
- console.log(chalk.cyan("━".repeat(60)));
2691
+ console.log(chalk.cyan(`\nšŸ“œ Fetching build logs...\n`));
2692
+
2693
+ const logsData = await getBuildLogs(
2694
+ apiUrl,
2695
+ token,
2696
+ accountId,
2697
+ session,
2698
+ serverless.ID,
2699
+ buildId
2700
+ );
2701
+
2702
+ if (!logsData || !logsData.data) {
2703
+ console.log(chalk.yellow("No logs found for this build."));
2704
+ return;
2705
+ }
2706
+
2707
+ console.log(chalk.cyan("━".repeat(80)));
2708
+ console.log(chalk.bold("Build Logs:\n"));
2709
+
2710
+ // Handle different log formats
2711
+ const logs = Array.isArray(logsData.data)
2712
+ ? logsData.data
2713
+ : [logsData.data];
2714
+
2715
+ logs.forEach((log) => {
2716
+ if (typeof log === "string") {
2717
+ console.log(log);
2718
+ } else if (log.Log) {
2719
+ // Log field contains the actual log content (may include ANSI colors)
2720
+ // Output directly to preserve color codes
2721
+ process.stdout.write(log.Log);
2722
+ if (!log.Log.endsWith("\n")) {
2723
+ process.stdout.write("\n");
2724
+ }
2725
+ } else if (log.Message || log.message) {
2726
+ const timestamp = log.Timestamp
2727
+ ? new Date(log.Timestamp * 1000).toLocaleTimeString()
2728
+ : "";
2729
+ console.log(
2730
+ chalk.dim(`[${timestamp}]`) +
2731
+ ` ${log.Message || log.message}`
2732
+ );
2733
+ } else {
2734
+ console.log(JSON.stringify(log, null, 2));
2735
+ }
2736
+ });
2737
+
2738
+ console.log("\n" + chalk.cyan("━".repeat(80)));
1925
2739
  } catch (error) {
1926
2740
  if (
1927
2741
  error.message &&