@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.
- package/README.md +64 -15
- package/api/serverless.js +148 -3
- package/cli.js +69 -34
- package/commands/env.js +11 -2
- package/commands/integration.js +11 -2
- package/commands/mcp.js +9 -2
- package/commands/serverless.js +1004 -188
- package/helper/verbose.js +62 -0
- package/package.json +1 -1
package/commands/serverless.js
CHANGED
|
@@ -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}`) +
|
|
1557
|
+
console.log(chalk.bold(` ${cmd.padEnd(12)}`) + details.description);
|
|
1542
1558
|
});
|
|
1543
1559
|
|
|
1544
|
-
console.log(chalk.cyan("\
|
|
1560
|
+
console.log(chalk.cyan("\nGlobal Options:\n"));
|
|
1545
1561
|
console.log(
|
|
1546
|
-
chalk.bold(" --
|
|
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(" --
|
|
1552
|
-
|
|
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(" --
|
|
1557
|
-
|
|
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(" --
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
|
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
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
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("\
|
|
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(" --
|
|
1586
|
-
|
|
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("\
|
|
1613
|
+
console.log(chalk.cyan("\nBuilds Options:\n"));
|
|
1591
1614
|
console.log(
|
|
1592
|
-
chalk.bold(" --name, -n") +
|
|
1593
|
-
|
|
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("\
|
|
1619
|
+
console.log(chalk.cyan("\nLogs Options:\n"));
|
|
1598
1620
|
console.log(
|
|
1599
|
-
chalk.
|
|
1600
|
-
"
|
|
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
|
-
"
|
|
1625
|
+
chalk.bold(" --follow, -f".padEnd(20)) + "Follow logs in real-time"
|
|
1607
1626
|
);
|
|
1608
1627
|
console.log(
|
|
1609
|
-
chalk.
|
|
1610
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1638
|
+
chalk.bold(" --build, -b".padEnd(20)) +
|
|
1639
|
+
"Build ID (prompts if not provided)"
|
|
1619
1640
|
);
|
|
1620
|
-
|
|
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
|
|
1646
|
+
" boltic serverless create -t blueprint -n my-api -l nodejs\n"
|
|
1623
1647
|
);
|
|
1624
1648
|
|
|
1625
|
-
console.log(chalk.
|
|
1626
|
-
console.log(
|
|
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.
|
|
1642
|
-
console.log(
|
|
1643
|
-
|
|
1644
|
-
console.log(chalk.dim(" #
|
|
1645
|
-
console.log(" boltic serverless
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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(
|
|
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(
|
|
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} |
|
|
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
|
-
|
|
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
|
-
|
|
1792
|
-
let name =
|
|
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
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
name = args[shortNameIndex + 1];
|
|
1939
|
+
// Enable verbose mode if requested
|
|
1940
|
+
if (verbose) {
|
|
1941
|
+
setVerboseMode(true);
|
|
1800
1942
|
}
|
|
1801
1943
|
|
|
1802
|
-
|
|
1944
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
1945
|
+
|
|
1946
|
+
// If name not provided, show list selector
|
|
1803
1947
|
if (!name) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
-
|
|
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
|
|
2071
|
+
name
|
|
1826
2072
|
);
|
|
1827
2073
|
|
|
1828
|
-
if (
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
1838
|
-
const
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
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(
|
|
2291
|
+
chalk.cyan(
|
|
2292
|
+
`\n🔨 Fetching builds for "${serverless.Config.Name}"...\n`
|
|
2293
|
+
)
|
|
1868
2294
|
);
|
|
1869
|
-
|
|
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.
|
|
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("
|
|
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.
|
|
1878
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1911
|
-
|
|
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
|
-
|
|
2628
|
+
|
|
2629
|
+
// If build ID not provided, fetch builds and let user select
|
|
2630
|
+
if (!buildId) {
|
|
1915
2631
|
console.log(
|
|
1916
|
-
chalk.cyan(
|
|
1917
|
-
|
|
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
|
-
|
|
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 &&
|