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