@boltic/cli 1.0.41 → 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
@@ -1538,132 +1554,184 @@ async function handlePull(args) {
1538
1554
  function showHelp() {
1539
1555
  console.log(chalk.cyan("\nServerless Commands:\n"));
1540
1556
  Object.entries(commands).forEach(([cmd, details]) => {
1541
- console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
1557
+ console.log(chalk.bold(` ${cmd.padEnd(12)}`) + details.description);
1542
1558
  });
1543
1559
 
1544
- console.log(chalk.cyan("\nCreate Command Options:\n"));
1560
+ console.log(chalk.cyan("\nGlobal Options:\n"));
1545
1561
  console.log(
1546
- chalk.bold(" --type, -t") +
1547
- chalk.dim(" ") +
1548
- "Serverless type: blueprint, git, or container (prompts if not provided)"
1562
+ chalk.bold(" --help, -h".padEnd(20)) + "Show help for a command"
1549
1563
  );
1564
+
1565
+ console.log(chalk.cyan("\nCreate Options:\n"));
1550
1566
  console.log(
1551
- chalk.bold(" --name, -n") +
1552
- chalk.dim(" ") +
1553
- "Name of the serverless function (required, prompts if not provided)"
1567
+ chalk.bold(" --type, -t".padEnd(20)) +
1568
+ "Serverless type: blueprint, git, or container"
1554
1569
  );
1555
1570
  console.log(
1556
- chalk.bold(" --language, -l") +
1557
- chalk.dim(" ") +
1558
- "Programming language: nodejs, python, golang, java (prompts if not provided)"
1571
+ chalk.bold(" --name, -n".padEnd(20)) +
1572
+ "Name of the serverless function"
1559
1573
  );
1560
1574
  console.log(
1561
- chalk.bold(" --directory, -d") +
1562
- chalk.dim(" ") +
1563
- "Directory where to create the project (default: current directory)"
1575
+ chalk.bold(" --language, -l".padEnd(20)) +
1576
+ "Programming language: nodejs, python, golang, java"
1577
+ );
1578
+ console.log(
1579
+ chalk.bold(" --directory, -d".padEnd(20)) +
1580
+ "Directory for the project (default: current)"
1564
1581
  );
1565
1582
 
1566
- console.log(chalk.cyan("\nTest Command Options:\n"));
1583
+ console.log(chalk.cyan("\nTest Options:\n"));
1567
1584
  console.log(
1568
- chalk.bold(" --port, -p") +
1569
- chalk.dim(" ") +
1585
+ chalk.bold(" --port, -p".padEnd(20)) +
1570
1586
  "Port to run the server on (default: 8080)"
1571
1587
  );
1572
1588
  console.log(
1573
- chalk.bold(" --language, -l") +
1574
- chalk.dim(" ") +
1575
- "Language (nodejs, python, golang, java) - auto-detected if not specified"
1589
+ chalk.bold(" --language, -l".padEnd(20)) +
1590
+ "Language (auto-detected if not specified)"
1591
+ );
1592
+ console.log(
1593
+ chalk.bold(" --directory, -d".padEnd(20)) +
1594
+ "Base directory of the project"
1576
1595
  );
1596
+
1597
+ console.log(chalk.cyan("\nPublish Options:\n"));
1577
1598
  console.log(
1578
- chalk.bold(" --directory, -d") +
1579
- chalk.dim(" ") +
1580
- "Base directory of the project (default: current directory)"
1599
+ chalk.bold(" --directory, -d".padEnd(20)) +
1600
+ "Directory of the serverless project"
1581
1601
  );
1582
1602
 
1583
- console.log(chalk.cyan("\nPublish 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
+ );
1584
1608
  console.log(
1585
- chalk.bold(" --directory, -d") +
1586
- chalk.dim(" ") +
1587
- "Directory of the serverless project (default: current directory)"
1609
+ chalk.bold(" --watch, -w".padEnd(20)) +
1610
+ "Poll until status is running, failed, or degraded"
1588
1611
  );
1589
1612
 
1590
- console.log(chalk.cyan("\nStatus Command Options:\n"));
1613
+ console.log(chalk.cyan("\nBuilds Options:\n"));
1591
1614
  console.log(
1592
- chalk.bold(" --name, -n") +
1593
- chalk.dim(" ") +
1594
- "Name of the serverless function (prompts if not provided)"
1615
+ chalk.bold(" --name, -n".padEnd(20)) +
1616
+ "Name of the serverless function"
1595
1617
  );
1596
1618
 
1597
- console.log(chalk.cyan("\nCreate Examples:\n"));
1619
+ console.log(chalk.cyan("\nLogs Options:\n"));
1598
1620
  console.log(
1599
- chalk.dim(
1600
- " # Interactive mode (will prompt for type, name, and language)"
1601
- )
1621
+ chalk.bold(" --name, -n".padEnd(20)) +
1622
+ "Name of the serverless function"
1602
1623
  );
1603
- console.log(" boltic serverless create\n");
1604
- console.log(chalk.dim(" # Create blueprint serverless"));
1605
1624
  console.log(
1606
- " boltic serverless create --type blueprint --name my-api --language nodejs\n"
1625
+ chalk.bold(" --follow, -f".padEnd(20)) + "Follow logs in real-time"
1607
1626
  );
1608
1627
  console.log(
1609
- chalk.dim(
1610
- " # Create git-based serverless (add your code, then publish)"
1611
- )
1628
+ chalk.bold(" --lines, -l".padEnd(20)) +
1629
+ "Number of lines to show (default: 100)"
1612
1630
  );
1631
+
1632
+ console.log(chalk.cyan("\nBuild Logs Options:\n"));
1613
1633
  console.log(
1614
- " boltic serverless create --type git --name my-git-func --language python\n"
1634
+ chalk.bold(" --name, -n".padEnd(20)) +
1635
+ "Name of the serverless function"
1615
1636
  );
1616
- console.log(chalk.dim(" # Create container-based serverless"));
1617
1637
  console.log(
1618
- " 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)"
1619
1640
  );
1620
- 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"));
1621
1645
  console.log(
1622
- " 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"
1623
1647
  );
1624
1648
 
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");
1649
+ console.log(chalk.dim(" # Test locally on port 3000"));
1650
+ console.log(" boltic serverless test -p 3000\n");
1630
1651
 
1631
- console.log(chalk.cyan("\nPublish Examples:\n"));
1632
1652
  console.log(chalk.dim(" # Publish from current directory"));
1633
1653
  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
1654
 
1637
- console.log(chalk.cyan("\nList Examples:\n"));
1638
1655
  console.log(chalk.dim(" # List all serverless functions"));
1639
1656
  console.log(" boltic serverless list\n");
1640
1657
 
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");
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");
1646
1669
  }
1647
1670
 
1648
1671
  // Execute the serverless command
1649
1672
  const execute = async (args) => {
1650
- const subCommand = args[0];
1651
-
1652
- 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
+ ) {
1653
1684
  showHelp();
1654
1685
  return;
1655
1686
  }
1656
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
+
1657
1694
  if (!commands[subCommand]) {
1658
- console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
1695
+ console.log(chalk.red(`Unknown serverless command: "${subCommand}"\n`));
1659
1696
  showHelp();
1660
1697
  return;
1661
1698
  }
1662
1699
 
1663
1700
  const commandObj = commands[subCommand];
1664
- await commandObj.action(args.slice(1));
1701
+ await commandObj.action(argsToPass);
1665
1702
  };
1666
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
+
1667
1735
  async function handleList(args = []) {
1668
1736
  try {
1669
1737
  const { apiUrl, token, accountId, session } = await getCurrentEnv();
@@ -1709,9 +1777,10 @@ async function handleList(args = []) {
1709
1777
  : "📝";
1710
1778
  const language = serverless.Config?.CodeOpts?.Language;
1711
1779
  const status = serverless.Status;
1780
+ const url = getServerlessUrl(serverless);
1712
1781
 
1713
1782
  return {
1714
- 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}` : ""}`,
1715
1784
  value: serverless,
1716
1785
  };
1717
1786
  });
@@ -1729,44 +1798,7 @@ async function handleList(args = []) {
1729
1798
 
1730
1799
  // Show details of selected serverless
1731
1800
  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
- );
1801
+ displayServerlessDetails(selected);
1770
1802
  }
1771
1803
  } catch (error) {
1772
1804
  if (
@@ -1783,143 +1815,927 @@ async function handleList(args = []) {
1783
1815
  }
1784
1816
  }
1785
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
+
1786
1931
  /**
1787
1932
  * Handle the status command - show status of a serverless function
1788
1933
  */
1789
1934
  async function handleStatus(args = []) {
1790
1935
  try {
1791
- // Parse name from args
1792
- let name = null;
1793
- const nameIndex = args.indexOf("--name");
1794
- const shortNameIndex = args.indexOf("-n");
1936
+ const parsedArgs = parseStatusArgs(args);
1937
+ let { name, watch, verbose, timeout } = parsedArgs;
1795
1938
 
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];
1939
+ // Enable verbose mode if requested
1940
+ if (verbose) {
1941
+ setVerboseMode(true);
1800
1942
  }
1801
1943
 
1802
- // If name not provided, prompt for it
1944
+ const { apiUrl, token, accountId, session } = await getCurrentEnv();
1945
+
1946
+ // If name not provided, show list selector
1803
1947
  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;
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
+ );
1811
1996
  },
1812
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
+ }
1813
2012
  }
1814
2013
 
1815
- 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`));
1816
2017
 
1817
- 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
+ );
2026
+
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
+ }
2038
+
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
2047
+ );
2048
+
2049
+ if (!serverless) {
2050
+ console.error(
2051
+ chalk.red("\n❌ Failed to fetch serverless details")
2052
+ );
2053
+ return;
2054
+ }
1818
2055
 
1819
- // Get serverless by name using query parameter
1820
- const result = await listAllServerless(
2056
+ displayServerlessDetails(serverless);
2057
+ return;
2058
+ }
2059
+
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`));
2064
+
2065
+ // First, get the serverless ID
2066
+ const initialResult = await listAllServerless(
1821
2067
  apiUrl,
1822
2068
  token,
1823
2069
  accountId,
1824
2070
  session,
1825
- name // Pass name as query parameter
2071
+ name
1826
2072
  );
1827
2073
 
1828
- if (!result || !Array.isArray(result)) {
1829
- console.error(
1830
- chalk.red(
1831
- "\n❌ Failed to fetch serverless: Invalid response format"
1832
- )
1833
- );
2074
+ if (
2075
+ !initialResult ||
2076
+ !Array.isArray(initialResult) ||
2077
+ !initialResult[0]
2078
+ ) {
2079
+ console.error(chalk.red(`\n❌ Serverless "${name}" not found.`));
1834
2080
  return;
1835
2081
  }
1836
2082
 
1837
- // Get first element (name is unique)
1838
- const serverless = result[0];
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();
1839
2088
 
1840
- if (!serverless) {
1841
- console.error(chalk.red(`\n❌ Serverless "${name}" not found.`));
1842
- console.log(
1843
- chalk.yellow(
1844
- "\nUse 'boltic serverless list' to see all serverless functions."
1845
- )
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
1846
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"));
1847
2157
  return;
1848
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
+ }
1849
2191
 
1850
- // Display status
2192
+ const choices = allServerless.map((serverless) => {
1851
2193
  const runtime = serverless.Config?.Runtime || "code";
1852
2194
  const typeIcon =
1853
2195
  runtime === "git" ? "📦" : runtime === "container" ? "🐳" : "📝";
1854
2196
  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
2197
 
1864
- console.log(chalk.cyan("━".repeat(60)));
1865
- 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
+
1866
2290
  console.log(
1867
- chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
2291
+ chalk.cyan(
2292
+ `\n🔨 Fetching builds for "${serverless.Config.Name}"...\n`
2293
+ )
1868
2294
  );
1869
- console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
2295
+
2296
+ const buildsData = await getServerlessBuilds(
2297
+ apiUrl,
2298
+ token,
2299
+ accountId,
2300
+ session,
2301
+ serverless.ID
2302
+ );
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)));
1870
2313
  console.log(
1871
- 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")
1872
2319
  );
1873
- 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";
1874
2332
 
1875
- if (serverless.Config?.CodeOpts?.Language) {
1876
2333
  console.log(
1877
- chalk.cyan(" Language: ") +
1878
- 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
1879
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;
1880
2370
  }
1881
- if (serverless.Config?.ContainerOpts?.Image) {
1882
- console.log(
1883
- chalk.cyan(" Image: ") +
1884
- 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:"
1885
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];
1886
2441
  }
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
- )
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
+ }
1893
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
+ }
1894
2530
  }
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
- )
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
1901
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];
1902
2606
  }
1903
- 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") {
1904
2611
  console.log(
1905
- chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
2612
+ chalk.yellow(
2613
+ `\n⚠️ Build logs are not available for container-type serverless functions.`
2614
+ )
1906
2615
  );
1907
- }
1908
- if (serverless.CreatedAt) {
1909
2616
  console.log(
1910
- chalk.cyan(" Created: ") +
1911
- chalk.white(new Date(serverless.CreatedAt).toLocaleString())
2617
+ chalk.dim(
2618
+ ` Container images are built externally and pulled directly.`
2619
+ )
1912
2620
  );
2621
+ console.log(
2622
+ chalk.dim(
2623
+ `\n To view runtime logs, use: boltic serverless logs ${serverless.Config.Name}`
2624
+ )
2625
+ );
2626
+ return;
1913
2627
  }
1914
- if (serverless.UpdatedAt) {
2628
+
2629
+ // If build ID not provided, fetch builds and let user select
2630
+ if (!buildId) {
1915
2631
  console.log(
1916
- chalk.cyan(" Updated: ") +
1917
- 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
1918
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;
1919
2689
  }
1920
2690
 
1921
- console.log();
1922
- 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)));
1923
2739
  } catch (error) {
1924
2740
  if (
1925
2741
  error.message &&