@boltic/cli 1.0.42 → 1.0.44-dev1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ import {
15
15
  parseTestArgs,
16
16
  parsePublishArgs,
17
17
  createServerlessFiles,
18
+ createGitignore,
18
19
  loadBolticConfig,
19
20
  parseLanguageFromConfig,
20
21
  parseHandlerConfig,
@@ -108,7 +109,7 @@ async function handleCreate(args = []) {
108
109
 
109
110
  // Step 1: Parse CLI arguments
110
111
  const parsedArgs = parseCreateArgs(args);
111
- let { name, language, directory, type } = parsedArgs;
112
+ let { name, language, directory, type, noGitignore } = parsedArgs;
112
113
 
113
114
  // Step 2: Serverless Type Selection
114
115
  if (!type) {
@@ -220,18 +221,30 @@ async function handleCreate(args = []) {
220
221
  // Branch based on type
221
222
  if (type === "git") {
222
223
  // For git type: create empty folder with boltic.yaml only
223
- await handleGitTypeCreate(name, language, version, targetDir);
224
+ await handleGitTypeCreate(
225
+ name,
226
+ language,
227
+ version,
228
+ targetDir,
229
+ noGitignore
230
+ );
224
231
  return;
225
232
  }
226
233
 
227
234
  if (type === "container") {
228
235
  // For container type: ask for image and create serverless
229
- await handleContainerTypeCreate(name, targetDir);
236
+ await handleContainerTypeCreate(name, targetDir, noGitignore);
230
237
  return;
231
238
  }
232
239
 
233
240
  // For code type: create full template files and call create API
234
- await handleCodeTypeCreate(name, language, version, targetDir);
241
+ await handleCodeTypeCreate(
242
+ name,
243
+ language,
244
+ version,
245
+ targetDir,
246
+ noGitignore
247
+ );
235
248
  } catch (error) {
236
249
  if (
237
250
  error.message &&
@@ -248,10 +261,87 @@ async function handleCreate(args = []) {
248
261
  }
249
262
  }
250
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
+
251
322
  /**
252
323
  * Handle code type serverless creation - creates folder with template files and calls create API
253
324
  */
254
- async function handleCodeTypeCreate(name, language, version, targetDir) {
325
+ async function handleCodeTypeCreate(
326
+ name,
327
+ language,
328
+ version,
329
+ targetDir,
330
+ noGitignore = false
331
+ ) {
332
+ // Check if serverless with this name already exists
333
+ const existingServerless = await checkServerlessExists(name);
334
+ if (existingServerless) {
335
+ displayServerlessExistsMessage(name, existingServerless);
336
+ // Cleanup the created directory
337
+ try {
338
+ fs.rmSync(targetDir, { recursive: true, force: true });
339
+ } catch {
340
+ // Ignore cleanup errors
341
+ }
342
+ return;
343
+ }
344
+
255
345
  const templateContext = {
256
346
  AppSlug: name,
257
347
  Language: `${language}/${version}`,
@@ -279,6 +369,14 @@ async function handleCodeTypeCreate(name, language, version, targetDir) {
279
369
  return;
280
370
  }
281
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
+
282
380
  // Get authentication credentials
283
381
  const env = await getCurrentEnv();
284
382
  if (!env || !env.token || !env.session) {
@@ -399,7 +497,26 @@ async function handleCodeTypeCreate(name, language, version, targetDir) {
399
497
  /**
400
498
  * Handle git type serverless creation - creates serverless on server and clones the repo
401
499
  */
402
- async function handleGitTypeCreate(name, language, version, targetDir) {
500
+ async function handleGitTypeCreate(
501
+ name,
502
+ language,
503
+ version,
504
+ targetDir,
505
+ noGitignore = false
506
+ ) {
507
+ // Check if serverless with this name already exists
508
+ const existingServerless = await checkServerlessExists(name);
509
+ if (existingServerless) {
510
+ displayServerlessExistsMessage(name, existingServerless);
511
+ // Cleanup the created directory
512
+ try {
513
+ fs.rmSync(targetDir, { recursive: true, force: true });
514
+ } catch {
515
+ // Ignore cleanup errors
516
+ }
517
+ return;
518
+ }
519
+
403
520
  console.log(chalk.cyan("\n📁 Creating git-based serverless project..."));
404
521
  console.log(chalk.dim(` Type: git`));
405
522
  console.log(chalk.dim(` Language: ${language}/${version}`));
@@ -539,6 +656,14 @@ serverlessConfig:
539
656
  }
540
657
  }
541
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
+
542
667
  // Display success message
543
668
  console.log("\n" + chalk.bgGreen.black(" ✓ CREATED ") + "\n");
544
669
  console.log(
@@ -624,7 +749,20 @@ serverlessConfig:
624
749
  /**
625
750
  * Handle container type serverless creation - creates empty folder with boltic.yaml
626
751
  */
627
- 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
+
628
766
  console.log(
629
767
  chalk.cyan("\n🐳 Creating container-based serverless project...")
630
768
  );
@@ -740,6 +878,14 @@ build:
740
878
  return;
741
879
  }
742
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
+
743
889
  // Display success message for container type
744
890
  console.log("\n" + chalk.bgGreen.black(" ✓ CREATED ") + "\n");
745
891
  console.log(
@@ -784,7 +930,12 @@ async function handlePublish(args = []) {
784
930
 
785
931
  // Step 1: Parse CLI arguments
786
932
  const parsedArgs = parsePublishArgs(args);
787
- const { directory } = parsedArgs;
933
+ const { directory, verbose } = parsedArgs;
934
+
935
+ // Enable verbose mode if requested
936
+ if (verbose) {
937
+ setVerboseMode(true);
938
+ }
788
939
 
789
940
  // Validate directory exists
790
941
  if (!fs.existsSync(directory)) {
@@ -837,10 +988,28 @@ async function handlePublish(args = []) {
837
988
  let code = null;
838
989
  if (runtime === "git") {
839
990
  console.log(
840
- chalk.red("\n📄 Git type serverless does not support publish")
991
+ chalk.yellow(
992
+ "\n📦 Git-based serverless deploys via git push, not publish."
993
+ )
994
+ );
995
+ console.log(chalk.cyan("\nTo deploy your changes:\n"));
996
+ console.log(chalk.white(" # Stage your changes"));
997
+ console.log(chalk.green(" git add .\n"));
998
+ console.log(chalk.white(" # Commit your changes"));
999
+ console.log(
1000
+ chalk.green(' git commit -m "Update serverless function"\n')
1001
+ );
1002
+ console.log(chalk.white(" # Push to deploy"));
1003
+ console.log(chalk.green(" git push origin main\n"));
1004
+ console.log(
1005
+ chalk.dim(
1006
+ "The serverless will automatically build and deploy after push."
1007
+ )
841
1008
  );
842
1009
  console.log(
843
- chalk.yellow("Please publish using git push origin main")
1010
+ chalk.dim(
1011
+ `Monitor status with: boltic serverless status --name ${appName} --follow\n`
1012
+ )
844
1013
  );
845
1014
  return;
846
1015
  }
@@ -1121,7 +1290,6 @@ async function handleTest(args = []) {
1121
1290
  // Install Python dependencies using virtual environment
1122
1291
  if (language === "python") {
1123
1292
  const venvPath = path.join(directory, ".venv");
1124
- const venvPython = path.join(venvPath, "bin", "python3");
1125
1293
  const venvPip = path.join(venvPath, "bin", "pip3");
1126
1294
 
1127
1295
  // Create virtual environment if it doesn't exist
@@ -1395,27 +1563,32 @@ async function handlePull(args) {
1395
1563
  try {
1396
1564
  // Parse command line arguments
1397
1565
  let currentDir = process.cwd();
1398
- const pathIndex = args.indexOf("--path");
1566
+ let serverlessName = null;
1399
1567
 
1400
- if (pathIndex !== -1 && args[pathIndex + 1]) {
1401
- currentDir = args[pathIndex + 1];
1402
- // Validate the provided path
1403
- if (!fs.existsSync(currentDir)) {
1404
- console.error(
1405
- chalk.red(
1406
- `Error: The specified path does not exist: ${currentDir}`
1407
- )
1408
- );
1409
- return;
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++;
1410
1578
  }
1411
1579
  }
1412
- const { apiUrl, token, accountId, session } = await getCurrentEnv();
1413
1580
 
1414
- console.log(
1415
- chalk.green(
1416
- "Please select the serverless to pull from the list below:"
1417
- )
1418
- );
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();
1419
1592
 
1420
1593
  const allServerless = await listAllServerless(
1421
1594
  apiUrl,
@@ -1429,42 +1602,85 @@ async function handlePull(args) {
1429
1602
  "\n❌ Failed to fetch serverless: Invalid response format"
1430
1603
  )
1431
1604
  );
1605
+ return;
1432
1606
  }
1433
1607
  if (allServerless.length === 0) {
1434
1608
  console.error(chalk.red("\n❌ No serverless found."));
1435
1609
  return;
1436
1610
  }
1437
- // Let user select an integration
1438
- const choices =
1439
- allServerless.map((serverless) => {
1440
- const runtime = serverless.Config?.Runtime || "code";
1441
- const typeIcon =
1442
- runtime === "git"
1443
- ? "📦"
1444
- : runtime === "container"
1445
- ? "🐳"
1446
- : "📝";
1447
- const language = serverless.Config?.CodeOpts?.Language;
1448
- return {
1449
- name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${serverless.Status}${language ? ` | language: ${language}` : ""}`,
1450
- value: serverless,
1451
- };
1452
- }) || [];
1453
1611
 
1454
- const selectedServerless = await search({
1455
- message: "Search and select an serverless to edit:",
1456
- source: async (term) => {
1457
- if (!term) return choices;
1458
- return choices?.filter((choice) =>
1459
- choice.name.toLowerCase().includes(term.toLowerCase())
1612
+ let selectedServerless;
1613
+
1614
+ // If name is provided, find exact match
1615
+ if (serverlessName) {
1616
+ selectedServerless = allServerless.find(
1617
+ (s) =>
1618
+ s.Config?.Name?.toLowerCase() ===
1619
+ serverlessName.toLowerCase()
1620
+ );
1621
+
1622
+ if (!selectedServerless) {
1623
+ console.error(
1624
+ chalk.red(`\n❌ Serverless "${serverlessName}" not found.`)
1460
1625
  );
1461
- },
1462
- });
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
+ }
1463
1640
 
1464
- console.log(
1465
- chalk.cyan("\nSelected serverless:"),
1466
- selectedServerless.Config.Name
1467
- );
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
+ }
1468
1684
  const pulledServerless = await pullServerless(
1469
1685
  apiUrl,
1470
1686
  token,
@@ -1482,12 +1698,9 @@ async function handlePull(args) {
1482
1698
  }
1483
1699
  // console.log("selectes serverless : ",pulledServerless)
1484
1700
 
1485
- // Get the app name, language and type for the folder name
1701
+ // Get the app name and type for the folder name
1486
1702
  const appName =
1487
1703
  pulledServerless?.Config?.Name || selectedServerless.Config?.Name;
1488
- const language =
1489
- pulledServerless?.Config?.CodeOpts?.Language?.split("/")[0] ||
1490
- "nodejs";
1491
1704
  const serverlessType = pulledServerless?.Config?.Runtime || "code";
1492
1705
 
1493
1706
  // Create folder name similar to create command
@@ -1606,7 +1819,7 @@ function showHelp() {
1606
1819
  "Name of the serverless function"
1607
1820
  );
1608
1821
  console.log(
1609
- chalk.bold(" --watch, -w".padEnd(20)) +
1822
+ chalk.bold(" --follow, -f".padEnd(20)) +
1610
1823
  "Poll until status is running, failed, or degraded"
1611
1824
  );
1612
1825
 
@@ -1656,7 +1869,7 @@ function showHelp() {
1656
1869
  console.log(" boltic serverless list\n");
1657
1870
 
1658
1871
  console.log(chalk.dim(" # Check status with polling"));
1659
- console.log(" boltic serverless status -n my-function --watch\n");
1872
+ console.log(" boltic serverless status -n my-function --follow\n");
1660
1873
 
1661
1874
  console.log(chalk.dim(" # View builds for a serverless"));
1662
1875
  console.log(" boltic serverless builds -n my-function\n");
@@ -1732,7 +1945,7 @@ function getStatusColor(status) {
1732
1945
  }
1733
1946
  }
1734
1947
 
1735
- async function handleList(args = []) {
1948
+ async function handleList(_args = []) {
1736
1949
  try {
1737
1950
  const { apiUrl, token, accountId, session } = await getCurrentEnv();
1738
1951
 
@@ -1889,7 +2102,7 @@ function displayServerlessDetails(serverless) {
1889
2102
  console.log(chalk.cyan("━".repeat(60)));
1890
2103
  console.log(
1891
2104
  chalk.dim(
1892
- "\nTip: Use 'boltic serverless status -n <name> --watch' to poll for status changes."
2105
+ "\nTip: Use 'boltic serverless status -n <name> --follow' to poll for status changes."
1893
2106
  )
1894
2107
  );
1895
2108
  }
@@ -1912,7 +2125,12 @@ function parseStatusArgs(args) {
1912
2125
  if ((arg === "--name" || arg === "-n") && nextArg) {
1913
2126
  parsed.name = nextArg;
1914
2127
  i++;
1915
- } else if (arg === "--watch" || arg === "-w") {
2128
+ } else if (
2129
+ arg === "--follow" ||
2130
+ arg === "-f" ||
2131
+ arg === "--watch" ||
2132
+ arg === "-w"
2133
+ ) {
1916
2134
  parsed.watch = true;
1917
2135
  } else if (arg === "--verbose" || arg === "-v") {
1918
2136
  parsed.verbose = true;
@@ -2132,7 +2350,7 @@ async function handleStatus(args = []) {
2132
2350
  lastStatus = status;
2133
2351
  } else if (iteration % 3 === 0) {
2134
2352
  // Show a dot every 3 iterations to indicate it's still polling
2135
- process.stdout.write(chalk.dim("."));
2353
+ process.stdout.write(chalk.dim(".\n"));
2136
2354
  }
2137
2355
 
2138
2356
  // Check if we've reached a terminal state
@@ -2450,7 +2668,10 @@ async function handleLogs(args = []) {
2450
2668
  console.log(chalk.dim("Following logs... Press Ctrl+C to stop.\n"));
2451
2669
  }
2452
2670
 
2453
- const fetchAndDisplayLogs = async (timestampEnd = null) => {
2671
+ // Track seen log IDs to avoid duplicates in follow mode
2672
+ const seenLogIds = new Set();
2673
+
2674
+ const fetchAndDisplayLogs = async (afterTimestamp = null) => {
2454
2675
  const now = Math.floor(Date.now() / 1000);
2455
2676
  const logsData = await getServerlessLogs(
2456
2677
  apiUrl,
@@ -2460,24 +2681,38 @@ async function handleLogs(args = []) {
2460
2681
  serverless.ID,
2461
2682
  {
2462
2683
  limit: lines,
2463
- timestampEnd: timestampEnd || now,
2464
- timestampStart: (timestampEnd || now) - 24 * 60 * 60,
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,
2465
2688
  }
2466
2689
  );
2467
2690
 
2468
2691
  if (!logsData || !logsData.data || logsData.data.length === 0) {
2469
- if (!follow) {
2692
+ if (!follow && !afterTimestamp) {
2470
2693
  console.log(
2471
2694
  chalk.yellow("No logs found for this serverless.")
2472
2695
  );
2473
2696
  }
2474
- return timestampEnd;
2697
+ return afterTimestamp;
2475
2698
  }
2476
2699
 
2477
2700
  const logs = logsData.data;
2478
- let latestTimestamp = timestampEnd;
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);
2479
2715
 
2480
- logs.forEach((log) => {
2481
2716
  // Timestamp is unix epoch in seconds
2482
2717
  const timestamp = log.Timestamp
2483
2718
  ? new Date(log.Timestamp * 1000).toLocaleTimeString()
@@ -2523,8 +2758,9 @@ async function handleLogs(args = []) {
2523
2758
  let lastTimestamp = await fetchAndDisplayLogs();
2524
2759
 
2525
2760
  if (follow) {
2761
+ // Poll for new logs every 2 seconds
2526
2762
  while (true) {
2527
- await new Promise((resolve) => setTimeout(resolve, 3000));
2763
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2528
2764
  lastTimestamp = await fetchAndDisplayLogs(lastTimestamp);
2529
2765
  }
2530
2766
  }
@@ -2548,9 +2784,10 @@ async function handleLogs(args = []) {
2548
2784
  */
2549
2785
  async function handleBuildLogs(args = []) {
2550
2786
  try {
2551
- // Parse args (supports --name, -n, or positional)
2787
+ // Parse args (supports --name, -n, --build, -b, --follow, -f)
2552
2788
  let name = null;
2553
2789
  let buildId = null;
2790
+ let follow = false;
2554
2791
 
2555
2792
  for (let i = 0; i < args.length; i++) {
2556
2793
  const arg = args[i];
@@ -2562,6 +2799,8 @@ async function handleBuildLogs(args = []) {
2562
2799
  } else if ((arg === "--build" || arg === "-b") && nextArg) {
2563
2800
  buildId = nextArg;
2564
2801
  i++;
2802
+ } else if (arg === "--follow" || arg === "-f") {
2803
+ follow = true;
2565
2804
  } else if (!arg.startsWith("-") && !name) {
2566
2805
  // Accept positional argument as name
2567
2806
  name = arg;
@@ -2690,50 +2929,98 @@ async function handleBuildLogs(args = []) {
2690
2929
 
2691
2930
  console.log(chalk.cyan(`\n📜 Fetching build logs...\n`));
2692
2931
 
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;
2932
+ if (follow) {
2933
+ console.log(
2934
+ chalk.dim("Following build logs... Press Ctrl+C to stop.\n")
2935
+ );
2705
2936
  }
2706
2937
 
2707
2938
  console.log(chalk.cyan("━".repeat(80)));
2708
2939
  console.log(chalk.bold("Build Logs:\n"));
2709
2940
 
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");
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."));
2724
2957
  }
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));
2958
+ return { hasLogs: false, buildComplete: false };
2735
2959
  }
2736
- });
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
+ }
2737
3024
 
2738
3025
  console.log("\n" + chalk.cyan("━".repeat(80)));
2739
3026
  } catch (error) {
@@ -14,10 +14,10 @@ import ora from "ora";
14
14
  // Supported languages and their versions
15
15
  export const SUPPORTED_LANGUAGES = ["nodejs", "python", "golang", "java"];
16
16
  export const LANGUAGE_VERSIONS = {
17
- nodejs: "20",
17
+ nodejs: "24",
18
18
  python: "3",
19
- golang: "1.22",
20
- java: "17",
19
+ golang: "1.24",
20
+ java: "21",
21
21
  };
22
22
 
23
23
  // Handler mapping per language
@@ -36,6 +36,233 @@ export const LANGUAGE_CHOICES = [
36
36
  { name: "Java", value: "java" },
37
37
  ];
38
38
 
39
+ // Language-specific .gitignore templates
40
+ export const GITIGNORE_TEMPLATES = {
41
+ nodejs: `# Dependencies
42
+ node_modules/
43
+ package-lock.json
44
+ yarn.lock
45
+ pnpm-lock.yaml
46
+
47
+ # Build output
48
+ dist/
49
+ build/
50
+ .next/
51
+
52
+ # Environment files
53
+ .env
54
+ .env.local
55
+ .env.*.local
56
+
57
+ # Logs
58
+ logs/
59
+ *.log
60
+ npm-debug.log*
61
+ yarn-debug.log*
62
+ yarn-error.log*
63
+
64
+ # IDE
65
+ .idea/
66
+ .vscode/
67
+ *.swp
68
+ *.swo
69
+ *~
70
+
71
+ # OS files
72
+ .DS_Store
73
+ Thumbs.db
74
+
75
+ # Boltic auto-generated files
76
+ autogen_*.js
77
+ `,
78
+ python: `# Byte-compiled / optimized / DLL files
79
+ __pycache__/
80
+ *.py[cod]
81
+ *$py.class
82
+
83
+ # Virtual environments
84
+ venv/
85
+ env/
86
+ .venv/
87
+ .env/
88
+
89
+ # Distribution / packaging
90
+ dist/
91
+ build/
92
+ *.egg-info/
93
+ .eggs/
94
+
95
+ # Environment files
96
+ .env
97
+ .env.local
98
+ .env.*.local
99
+
100
+ # Logs
101
+ *.log
102
+
103
+ # IDE
104
+ .idea/
105
+ .vscode/
106
+ *.swp
107
+ *.swo
108
+ *~
109
+
110
+ # OS files
111
+ .DS_Store
112
+ Thumbs.db
113
+
114
+ # Boltic auto-generated files
115
+ autogen_*.py
116
+ `,
117
+ golang: `# Binaries
118
+ *.exe
119
+ *.exe~
120
+ *.dll
121
+ *.so
122
+ *.dylib
123
+
124
+ # Build output
125
+ bin/
126
+ /main
127
+
128
+ # Test binary
129
+ *.test
130
+
131
+ # Go workspace
132
+ go.work
133
+ go.work.sum
134
+
135
+ # Vendor directory (if not committing dependencies)
136
+ # vendor/
137
+
138
+ # Environment files
139
+ .env
140
+ .env.local
141
+ .env.*.local
142
+
143
+ # Logs
144
+ *.log
145
+
146
+ # IDE
147
+ .idea/
148
+ .vscode/
149
+ *.swp
150
+ *.swo
151
+ *~
152
+
153
+ # OS files
154
+ .DS_Store
155
+ Thumbs.db
156
+
157
+ # Boltic auto-generated files
158
+ autogen_*.go
159
+ `,
160
+ java: `# Compiled class files
161
+ *.class
162
+
163
+ # Build output
164
+ target/
165
+ build/
166
+ out/
167
+ bin/
168
+
169
+ # Package files
170
+ *.jar
171
+ *.war
172
+ *.ear
173
+
174
+ # Maven
175
+ .mvn/
176
+ !.mvn/wrapper/maven-wrapper.jar
177
+ pom.xml.tag
178
+ pom.xml.releaseBackup
179
+ pom.xml.versionsBackup
180
+ pom.xml.next
181
+
182
+ # Gradle
183
+ .gradle/
184
+ gradle/
185
+ gradlew
186
+ gradlew.bat
187
+
188
+ # Environment files
189
+ .env
190
+ .env.local
191
+ .env.*.local
192
+
193
+ # Logs
194
+ *.log
195
+
196
+ # IDE
197
+ .idea/
198
+ .vscode/
199
+ *.iml
200
+ *.ipr
201
+ *.iws
202
+ .classpath
203
+ .project
204
+ .settings/
205
+ *.swp
206
+ *.swo
207
+ *~
208
+
209
+ # OS files
210
+ .DS_Store
211
+ Thumbs.db
212
+
213
+ # Boltic auto-generated files
214
+ AutogenIndex.java
215
+ `,
216
+ container: `# Environment files
217
+ .env
218
+ .env.local
219
+ .env.*.local
220
+
221
+ # Logs
222
+ *.log
223
+
224
+ # IDE
225
+ .idea/
226
+ .vscode/
227
+ *.swp
228
+ *.swo
229
+ *~
230
+
231
+ # OS files
232
+ .DS_Store
233
+ Thumbs.db
234
+
235
+ # Docker
236
+ .docker/
237
+ `,
238
+ };
239
+
240
+ /**
241
+ * Create a .gitignore file for the serverless project
242
+ * @param {string} targetDir - Directory to create .gitignore in
243
+ * @param {string} language - Programming language (nodejs, python, golang, java, container)
244
+ * @returns {boolean} - True if created successfully
245
+ */
246
+ export function createGitignore(targetDir, language) {
247
+ const gitignorePath = path.join(targetDir, ".gitignore");
248
+
249
+ // Don't overwrite existing .gitignore
250
+ if (fs.existsSync(gitignorePath)) {
251
+ return false;
252
+ }
253
+
254
+ const template =
255
+ GITIGNORE_TEMPLATES[language] || GITIGNORE_TEMPLATES.nodejs;
256
+
257
+ try {
258
+ fs.writeFileSync(gitignorePath, template);
259
+ return true;
260
+ } catch {
261
+ // Silently fail - .gitignore is optional
262
+ return false;
263
+ }
264
+ }
265
+
39
266
  /**
40
267
  * Parse command line arguments for the create command
41
268
  */
@@ -45,6 +272,7 @@ export function parseCreateArgs(args) {
45
272
  language: null,
46
273
  directory: process.cwd(),
47
274
  type: null, // code, git, or container
275
+ noGitignore: false, // Skip .gitignore creation
48
276
  };
49
277
 
50
278
  for (let i = 0; i < args.length; i++) {
@@ -68,6 +296,8 @@ export function parseCreateArgs(args) {
68
296
  }
69
297
  parsed.type = typeValue;
70
298
  i++;
299
+ } else if (arg === "--no-gitignore") {
300
+ parsed.noGitignore = true;
71
301
  }
72
302
  }
73
303
 
@@ -705,7 +935,7 @@ public class AutogenIndex {
705
935
  function getGoModContent(appName) {
706
936
  return `module ${appName}
707
937
 
708
- go 1.22
938
+ go 1.24
709
939
  `;
710
940
  }
711
941
 
@@ -733,7 +963,7 @@ function getJavaPomXmlContent(appName) {
733
963
  <description>Boltic Serverless Function</description>
734
964
 
735
965
  <properties>
736
- <java.version>17</java.version>
966
+ <java.version>21</java.version>
737
967
  </properties>
738
968
 
739
969
  <dependencies>
@@ -1071,6 +1301,7 @@ export function displayTestStartupMessage(port) {
1071
1301
  export function parsePublishArgs(args) {
1072
1302
  const parsed = {
1073
1303
  directory: process.cwd(),
1304
+ verbose: false,
1074
1305
  };
1075
1306
 
1076
1307
  for (let i = 0; i < args.length; i++) {
@@ -1080,6 +1311,8 @@ export function parsePublishArgs(args) {
1080
1311
  if ((arg === "--directory" || arg === "-d") && nextArg) {
1081
1312
  parsed.directory = path.resolve(nextArg);
1082
1313
  i++;
1314
+ } else if (arg === "--verbose" || arg === "-v") {
1315
+ parsed.verbose = true;
1083
1316
  } else if (!arg.startsWith("-") && !parsed._dirSet) {
1084
1317
  // Accept positional argument as directory (e.g., `boltic serverless publish ./my-project`)
1085
1318
  parsed.directory = path.resolve(arg);
@@ -1207,7 +1440,7 @@ export function displayPublishSuccessMessage(name, response) {
1207
1440
  export function getPulledBolticYamlContent(serverlessData) {
1208
1441
  const config = serverlessData.Config;
1209
1442
  const runtime = config.Runtime || "code";
1210
- const language = config.CodeOpts?.Language || "nodejs/20";
1443
+ const language = config.CodeOpts?.Language || "nodejs/24";
1211
1444
  const handler =
1212
1445
  HANDLER_MAPPING[language.split("/")[0]] || "handler.handler";
1213
1446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boltic/cli",
3
- "version": "1.0.42",
3
+ "version": "1.0.44-dev1.1",
4
4
  "description": "Professional CLI for interacting with the Boltic platform — create, manage, and publish integrations, serverless functions, workflows, MCPs, and more with enterprise-grade features and a seamless developer experience",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -136,4 +136,4 @@
136
136
  "x64",
137
137
  "arm64"
138
138
  ]
139
- }
139
+ }