openkbs 0.0.55 → 0.0.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openkbs",
3
- "version": "0.0.55",
3
+ "version": "0.0.59",
4
4
  "description": "OpenKBS - Command Line Interface",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/actions.js CHANGED
@@ -17,6 +17,79 @@ const TEMPLATE_DIR = path.join(os.homedir(), '.openkbs', 'templates');
17
17
  const jwtPath = path.join(os.homedir(), '.openkbs', 'clientJWT');
18
18
  const generateTransactionId = () => `${+new Date()}-${Math.floor(100000 + Math.random() * 900000)}`;
19
19
 
20
+ /**
21
+ * Find settings from settings.json - checks current dir, then functions/ or site/ subdirs
22
+ * Returns full settings object with kbId, region, etc.
23
+ */
24
+ function findSettings() {
25
+ const paths = [
26
+ path.join(process.cwd(), 'settings.json'),
27
+ path.join(process.cwd(), 'app', 'settings.json'),
28
+ path.join(process.cwd(), 'functions', 'settings.json'),
29
+ path.join(process.cwd(), 'site', 'settings.json')
30
+ ];
31
+
32
+ for (const settingsPath of paths) {
33
+ if (fs.existsSync(settingsPath)) {
34
+ try {
35
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
36
+ if (settings.kbId) return settings;
37
+ } catch (e) {}
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Find kbId from settings.json - checks current dir, then functions/ or site/ subdirs
45
+ */
46
+ function findKbId() {
47
+ const settings = findSettings();
48
+ return settings?.kbId || null;
49
+ }
50
+
51
+ /**
52
+ * Find region from settings.json - checks current dir, then functions/ or site/ subdirs
53
+ * Default: 'us-east-1'
54
+ */
55
+ function findRegion() {
56
+ const settings = findSettings();
57
+ return settings?.region || 'us-east-1';
58
+ }
59
+
60
+ // MIME types for common file extensions
61
+ const MIME_TYPES = {
62
+ '.html': 'text/html',
63
+ '.htm': 'text/html',
64
+ '.css': 'text/css',
65
+ '.js': 'application/javascript',
66
+ '.mjs': 'application/javascript',
67
+ '.json': 'application/json',
68
+ '.png': 'image/png',
69
+ '.jpg': 'image/jpeg',
70
+ '.jpeg': 'image/jpeg',
71
+ '.gif': 'image/gif',
72
+ '.svg': 'image/svg+xml',
73
+ '.ico': 'image/x-icon',
74
+ '.webp': 'image/webp',
75
+ '.woff': 'font/woff',
76
+ '.woff2': 'font/woff2',
77
+ '.ttf': 'font/ttf',
78
+ '.eot': 'application/vnd.ms-fontobject',
79
+ '.pdf': 'application/pdf',
80
+ '.txt': 'text/plain',
81
+ '.xml': 'application/xml',
82
+ '.zip': 'application/zip',
83
+ '.mp3': 'audio/mpeg',
84
+ '.mp4': 'video/mp4',
85
+ '.webm': 'video/webm'
86
+ };
87
+
88
+ function getMimeType(filePath) {
89
+ const ext = path.extname(filePath).toLowerCase();
90
+ return MIME_TYPES[ext] || 'application/octet-stream';
91
+ }
92
+
20
93
  async function signAction(options) {
21
94
  try {
22
95
  const userProfile = await getUserProfile();
@@ -840,11 +913,17 @@ async function downloadClaudeMdFromS3(claudeMdPath) {
840
913
  // ===== Elastic Functions Commands =====
841
914
 
842
915
  async function fnAction(subCommand, args = []) {
843
- const localKBData = await fetchLocalKBData();
844
- const kbId = localKBData?.kbId;
916
+ // Find kbId from settings.json (current dir, app/, functions/, site/)
917
+ let kbId = findKbId();
918
+
919
+ if (!kbId) {
920
+ // Fallback to standard KB lookup
921
+ const localKBData = await fetchLocalKBData();
922
+ kbId = localKBData?.kbId;
923
+ }
845
924
 
846
925
  if (!kbId) {
847
- return console.red('No KB found. Please run this command in a KB project directory.');
926
+ return console.red('No KB found. Create settings.json with {"kbId": "..."} or run from a KB project directory.');
848
927
  }
849
928
 
850
929
  const { kbToken } = await fetchKBJWT(kbId);
@@ -874,7 +953,7 @@ async function fnAction(subCommand, args = []) {
874
953
  console.log(' invoke <name> [payload] Invoke a function');
875
954
  console.log('');
876
955
  console.log('Options for deploy:');
877
- console.log(' --region <region> Region (us-east-2, eu-central-1, ap-southeast-1)');
956
+ console.log(' --region <region> Region (us-east-1, eu-central-1, ap-southeast-1)');
878
957
  console.log(' --memory <mb> Memory size (128-3008 MB)');
879
958
  console.log(' --timeout <seconds> Timeout (1-900 seconds)');
880
959
  }
@@ -899,7 +978,7 @@ async function fnListAction(kbToken) {
899
978
  console.log('Create a function:');
900
979
  console.log(' 1. Create directory: mkdir -p functions/hello');
901
980
  console.log(' 2. Create handler: echo "export const handler = async (event) => ({ body: \'Hello!\' });" > functions/hello/index.mjs');
902
- console.log(' 3. Deploy: openkbs fn deploy hello --region us-east-2');
981
+ console.log(' 3. Deploy: openkbs fn deploy hello --region us-east-1');
903
982
  return;
904
983
  }
905
984
 
@@ -922,8 +1001,8 @@ async function fnDeployAction(kbToken, functionName, args) {
922
1001
  return console.red('Function name required. Usage: openkbs fn deploy <name>');
923
1002
  }
924
1003
 
925
- // Parse arguments
926
- let region = 'us-east-2';
1004
+ // Parse arguments - region defaults to settings.json or us-east-1
1005
+ let region = findRegion();
927
1006
  let memorySize = 256;
928
1007
  let timeout = 30;
929
1008
 
@@ -937,15 +1016,28 @@ async function fnDeployAction(kbToken, functionName, args) {
937
1016
  }
938
1017
  }
939
1018
 
940
- const functionDir = path.join(process.cwd(), 'functions', functionName);
1019
+ // Try to find the function directory in order:
1020
+ // 1. ./functionName (if running from functions/ directory)
1021
+ // 2. ./functions/functionName (if running from project root)
1022
+ let functionDir = path.join(process.cwd(), functionName);
1023
+ if (!await fs.pathExists(functionDir)) {
1024
+ functionDir = path.join(process.cwd(), 'functions', functionName);
1025
+ }
941
1026
 
942
1027
  if (!await fs.pathExists(functionDir)) {
943
- return console.red(`Function directory not found: ${functionDir}`);
1028
+ return console.red(`Function directory not found. Tried:\n - ./${functionName}\n - ./functions/${functionName}`);
944
1029
  }
945
1030
 
946
1031
  console.log(`Deploying function '${functionName}' to ${region}...`);
947
1032
 
948
1033
  try {
1034
+ // Check if package.json exists and run npm install
1035
+ const packageJsonPath = path.join(functionDir, 'package.json');
1036
+ if (await fs.pathExists(packageJsonPath)) {
1037
+ console.log('Installing dependencies...');
1038
+ execSync('npm install --production', { cwd: functionDir, stdio: 'inherit' });
1039
+ }
1040
+
949
1041
  // Create a zip of the function directory
950
1042
  const archiver = require('archiver');
951
1043
  const { PassThrough } = require('stream');
@@ -1180,6 +1272,847 @@ async function fnInvokeAction(kbToken, functionName, args) {
1180
1272
  }
1181
1273
  }
1182
1274
 
1275
+ // ===== Elastic Storage Commands =====
1276
+
1277
+ async function storageAction(subCommand, args = []) {
1278
+ // Find kbId from settings.json (current dir, app/, functions/, site/)
1279
+ let kbId = findKbId();
1280
+
1281
+ if (!kbId) {
1282
+ const localKBData = await fetchLocalKBData();
1283
+ kbId = localKBData?.kbId;
1284
+ }
1285
+
1286
+ if (!kbId) {
1287
+ return console.red('No KB found. Create settings.json with {"kbId": "..."} or run from a KB project directory.');
1288
+ }
1289
+
1290
+ const { kbToken } = await fetchKBJWT(kbId);
1291
+
1292
+ switch (subCommand) {
1293
+ case 'enable':
1294
+ return await storageEnableAction(kbToken);
1295
+ case 'status':
1296
+ return await storageStatusAction(kbToken);
1297
+ case 'ls':
1298
+ case 'list':
1299
+ return await storageListAction(kbToken, args[0]);
1300
+ case 'put':
1301
+ case 'upload':
1302
+ return await storageUploadAction(kbToken, args[0], args[1]);
1303
+ case 'get':
1304
+ case 'download':
1305
+ return await storageDownloadAction(kbToken, args[0], args[1]);
1306
+ case 'rm':
1307
+ case 'delete':
1308
+ return await storageDeleteAction(kbToken, args[0]);
1309
+ case 'disable':
1310
+ return await storageDisableAction(kbToken, args);
1311
+ case 'cloudfront':
1312
+ case 'cf':
1313
+ return await storageCloudFrontAction(kbToken, args[0], args[1]);
1314
+ case 'public':
1315
+ return await storagePublicAction(kbToken, args[0]);
1316
+ default:
1317
+ console.log('Usage: openkbs storage <command> [options]');
1318
+ console.log('');
1319
+ console.log('Commands:');
1320
+ console.log(' enable Enable elastic storage for this KB');
1321
+ console.log(' status Show storage status and info');
1322
+ console.log(' public <true|false> Make storage publicly readable');
1323
+ console.log(' ls [prefix] List objects in storage');
1324
+ console.log(' put <local> <remote> Upload a file to storage');
1325
+ console.log(' get <remote> <local> Download a file from storage');
1326
+ console.log(' rm <key> Delete an object from storage');
1327
+ console.log(' disable [--force] Disable storage (deletes bucket)');
1328
+ console.log(' cloudfront <path> Add storage to CloudFront at path (e.g., /media)');
1329
+ console.log(' cloudfront remove <path> Remove storage from CloudFront');
1330
+ }
1331
+ }
1332
+
1333
+ async function storageEnableAction(kbToken) {
1334
+ try {
1335
+ console.log('Enabling elastic storage...');
1336
+
1337
+ const response = await makePostRequest(KB_API_URL, {
1338
+ token: kbToken,
1339
+ action: 'enableElasticStorage'
1340
+ });
1341
+
1342
+ if (response.error) {
1343
+ return console.red('Error:', response.error);
1344
+ }
1345
+
1346
+ if (response.alreadyEnabled) {
1347
+ console.yellow('Storage is already enabled.');
1348
+ } else {
1349
+ console.green('Storage enabled successfully!');
1350
+ }
1351
+
1352
+ console.log(` Bucket: ${response.bucket}`);
1353
+ console.log(` Region: ${response.region}`);
1354
+
1355
+ if (response.functionsUpdated > 0) {
1356
+ console.log(` Updated ${response.functionsUpdated} function(s) with STORAGE_BUCKET env var`);
1357
+ }
1358
+ } catch (error) {
1359
+ console.red('Error enabling storage:', error.message);
1360
+ }
1361
+ }
1362
+
1363
+ async function storageStatusAction(kbToken) {
1364
+ try {
1365
+ const response = await makePostRequest(KB_API_URL, {
1366
+ token: kbToken,
1367
+ action: 'getElasticStorage'
1368
+ });
1369
+
1370
+ if (response.error) {
1371
+ return console.red('Error:', response.error);
1372
+ }
1373
+
1374
+ if (!response.enabled) {
1375
+ console.log('Elastic Storage: disabled');
1376
+ console.log('');
1377
+ console.log('Enable with: openkbs storage enable');
1378
+ return;
1379
+ }
1380
+
1381
+ console.log('Elastic Storage: enabled');
1382
+ console.log(` Bucket: ${response.bucket}`);
1383
+ console.log(` Region: ${response.region}`);
1384
+ console.log(` Public: ${response.public ? 'yes' : 'no'}`);
1385
+ if (response.publicUrl) {
1386
+ console.log(` Public URL: ${response.publicUrl}`);
1387
+ }
1388
+ } catch (error) {
1389
+ console.red('Error getting storage status:', error.message);
1390
+ }
1391
+ }
1392
+
1393
+ async function storageListAction(kbToken, prefix = '') {
1394
+ try {
1395
+ const response = await makePostRequest(KB_API_URL, {
1396
+ token: kbToken,
1397
+ action: 'listStorageObjects',
1398
+ prefix: prefix || ''
1399
+ });
1400
+
1401
+ if (response.error) {
1402
+ return console.red('Error:', response.error);
1403
+ }
1404
+
1405
+ const objects = response.objects || [];
1406
+
1407
+ if (objects.length === 0) {
1408
+ console.log('No objects found.');
1409
+ return;
1410
+ }
1411
+
1412
+ console.log(`Objects${prefix ? ` (prefix: ${prefix})` : ''}:\n`);
1413
+
1414
+ objects.forEach(obj => {
1415
+ const size = formatBytes(obj.size);
1416
+ const date = new Date(obj.lastModified).toISOString().split('T')[0];
1417
+ console.log(` ${date} ${size.padStart(10)} ${obj.key}`);
1418
+ });
1419
+
1420
+ if (response.isTruncated) {
1421
+ console.log('\n (more objects exist, use prefix to filter)');
1422
+ }
1423
+ } catch (error) {
1424
+ console.red('Error listing objects:', error.message);
1425
+ }
1426
+ }
1427
+
1428
+ async function storageUploadAction(kbToken, localPath, remoteKey) {
1429
+ if (!localPath) {
1430
+ return console.red('Usage: openkbs storage put <local-file> [remote-key]');
1431
+ }
1432
+
1433
+ const fullLocalPath = path.resolve(localPath);
1434
+
1435
+ if (!fs.existsSync(fullLocalPath)) {
1436
+ return console.red(`File not found: ${fullLocalPath}`);
1437
+ }
1438
+
1439
+ // Use filename if remote key not specified
1440
+ if (!remoteKey) {
1441
+ remoteKey = path.basename(localPath);
1442
+ }
1443
+
1444
+ try {
1445
+ console.log(`Uploading ${localPath} to ${remoteKey}...`);
1446
+
1447
+ // Get presigned upload URL
1448
+ const response = await makePostRequest(KB_API_URL, {
1449
+ token: kbToken,
1450
+ action: 'getStorageUploadUrl',
1451
+ storageKey: remoteKey
1452
+ });
1453
+
1454
+ if (response.error) {
1455
+ return console.red('Error:', response.error);
1456
+ }
1457
+
1458
+ // Upload file using presigned URL
1459
+ const fileContent = fs.readFileSync(fullLocalPath);
1460
+ await fetch(response.uploadUrl, {
1461
+ method: 'PUT',
1462
+ body: fileContent
1463
+ });
1464
+
1465
+ console.green(`Uploaded: ${remoteKey}`);
1466
+
1467
+ if (response.publicUrl) {
1468
+ console.log(`Public URL: ${response.publicUrl}`);
1469
+ }
1470
+ } catch (error) {
1471
+ console.red('Upload failed:', error.message);
1472
+ }
1473
+ }
1474
+
1475
+ async function storageDownloadAction(kbToken, remoteKey, localPath) {
1476
+ if (!remoteKey) {
1477
+ return console.red('Usage: openkbs storage get <remote-key> [local-file]');
1478
+ }
1479
+
1480
+ // Use remote filename if local path not specified
1481
+ if (!localPath) {
1482
+ localPath = path.basename(remoteKey);
1483
+ }
1484
+
1485
+ try {
1486
+ console.log(`Downloading ${remoteKey} to ${localPath}...`);
1487
+
1488
+ // Get presigned download URL
1489
+ const response = await makePostRequest(KB_API_URL, {
1490
+ token: kbToken,
1491
+ action: 'getStorageDownloadUrl',
1492
+ storageKey: remoteKey
1493
+ });
1494
+
1495
+ if (response.error) {
1496
+ return console.red('Error:', response.error);
1497
+ }
1498
+
1499
+ // Download file
1500
+ const fetchResponse = await fetch(response.downloadUrl);
1501
+ if (!fetchResponse.ok) {
1502
+ return console.red(`Download failed: ${fetchResponse.statusText}`);
1503
+ }
1504
+
1505
+ const buffer = await fetchResponse.arrayBuffer();
1506
+ fs.writeFileSync(localPath, Buffer.from(buffer));
1507
+
1508
+ console.green(`Downloaded: ${localPath}`);
1509
+ } catch (error) {
1510
+ console.red('Download failed:', error.message);
1511
+ }
1512
+ }
1513
+
1514
+ async function storageDeleteAction(kbToken, key) {
1515
+ if (!key) {
1516
+ return console.red('Usage: openkbs storage rm <key>');
1517
+ }
1518
+
1519
+ try {
1520
+ console.log(`Deleting ${key}...`);
1521
+
1522
+ const response = await makePostRequest(KB_API_URL, {
1523
+ token: kbToken,
1524
+ action: 'deleteStorageObject',
1525
+ storageKey: key
1526
+ });
1527
+
1528
+ if (response.error) {
1529
+ return console.red('Error:', response.error);
1530
+ }
1531
+
1532
+ console.green(`Deleted: ${key}`);
1533
+ } catch (error) {
1534
+ console.red('Delete failed:', error.message);
1535
+ }
1536
+ }
1537
+
1538
+ async function storageDisableAction(kbToken, args) {
1539
+ const force = args.includes('--force');
1540
+
1541
+ try {
1542
+ console.log('Disabling elastic storage...');
1543
+
1544
+ if (!force) {
1545
+ console.yellow('Warning: This will delete the storage bucket.');
1546
+ console.yellow('Use --force to delete all objects and the bucket.');
1547
+ return;
1548
+ }
1549
+
1550
+ const response = await makePostRequest(KB_API_URL, {
1551
+ token: kbToken,
1552
+ action: 'deleteElasticStorage',
1553
+ force: true
1554
+ });
1555
+
1556
+ if (response.error) {
1557
+ return console.red('Error:', response.error);
1558
+ }
1559
+
1560
+ console.green('Storage disabled successfully.');
1561
+
1562
+ if (response.functionsUpdated > 0) {
1563
+ console.log(`Removed STORAGE_BUCKET from ${response.functionsUpdated} function(s)`);
1564
+ }
1565
+ } catch (error) {
1566
+ console.red('Error disabling storage:', error.message);
1567
+ }
1568
+ }
1569
+
1570
+ async function storagePublicAction(kbToken, value) {
1571
+ if (!value || !['true', 'false'].includes(value.toLowerCase())) {
1572
+ return console.red('Usage: openkbs storage public <true|false>');
1573
+ }
1574
+
1575
+ const makePublic = value.toLowerCase() === 'true';
1576
+
1577
+ try {
1578
+ console.log(makePublic ? 'Making storage public...' : 'Making storage private...');
1579
+
1580
+ const response = await makePostRequest(KB_API_URL, {
1581
+ token: kbToken,
1582
+ action: 'setElasticStoragePublic',
1583
+ isPublic: makePublic
1584
+ });
1585
+
1586
+ if (response.error) {
1587
+ return console.red('Error:', response.error);
1588
+ }
1589
+
1590
+ if (makePublic) {
1591
+ console.green('Storage is now public!');
1592
+ console.log(`Public URL: ${response.publicUrl}`);
1593
+ } else {
1594
+ console.green('Storage is now private.');
1595
+ }
1596
+ } catch (error) {
1597
+ console.red('Error:', error.message);
1598
+ }
1599
+ }
1600
+
1601
+ async function storageCloudFrontAction(kbToken, pathOrRemove, pathArg) {
1602
+ // Handle "storage cloudfront remove <path>" vs "storage cloudfront <path>"
1603
+ let pathPrefix;
1604
+ let enable = true;
1605
+
1606
+ if (pathOrRemove === 'remove' || pathOrRemove === 'rm') {
1607
+ if (!pathArg) {
1608
+ return console.red('Usage: openkbs storage cloudfront remove <path>');
1609
+ }
1610
+ pathPrefix = pathArg;
1611
+ enable = false;
1612
+ } else {
1613
+ if (!pathOrRemove) {
1614
+ console.log('Usage: openkbs storage cloudfront <path>');
1615
+ console.log(' openkbs storage cloudfront remove <path>');
1616
+ console.log('');
1617
+ console.log('Examples:');
1618
+ console.log(' openkbs storage cloudfront media # Makes storage available at /media/*');
1619
+ console.log(' openkbs storage cloudfront files # Makes storage available at /files/*');
1620
+ console.log(' openkbs storage cloudfront remove media # Remove from CloudFront');
1621
+ return;
1622
+ }
1623
+ pathPrefix = pathOrRemove;
1624
+ }
1625
+
1626
+ try {
1627
+ if (enable) {
1628
+ console.log(`Adding storage to CloudFront at /${pathPrefix}/*...`);
1629
+ } else {
1630
+ console.log(`Removing storage from CloudFront at /${pathPrefix}/*...`);
1631
+ }
1632
+
1633
+ const response = await makePostRequest(KB_API_URL, {
1634
+ token: kbToken,
1635
+ action: 'setStorageCloudFront',
1636
+ pathPrefix,
1637
+ enable
1638
+ });
1639
+
1640
+ if (response.error) {
1641
+ return console.red('Error:', response.error);
1642
+ }
1643
+
1644
+ if (enable) {
1645
+ console.green('Storage added to CloudFront!');
1646
+ console.log(` Custom URL: ${response.customUrl}`);
1647
+ console.log(` Path: /${response.path}/*`);
1648
+ console.yellow('\n Note: CloudFront changes take 2-5 minutes to propagate.');
1649
+ } else {
1650
+ console.green('Storage removed from CloudFront.');
1651
+ console.yellow(' Note: CloudFront changes take 2-5 minutes to propagate.');
1652
+ }
1653
+ } catch (error) {
1654
+ console.red('Error:', error.message);
1655
+ }
1656
+ }
1657
+
1658
+ function formatBytes(bytes) {
1659
+ if (bytes === 0) return '0 B';
1660
+ const k = 1024;
1661
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1662
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1663
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
1664
+ }
1665
+
1666
+ // ===== Elastic Postgres Commands =====
1667
+
1668
+ async function postgresAction(subCommand, args = []) {
1669
+ // Find kbId from settings.json (current dir, app/, functions/, site/)
1670
+ let kbId = findKbId();
1671
+
1672
+ if (!kbId) {
1673
+ const localKBData = await fetchLocalKBData();
1674
+ kbId = localKBData?.kbId;
1675
+ }
1676
+
1677
+ if (!kbId) {
1678
+ return console.red('No KB found. Create settings.json with {"kbId": "..."} or run from a KB project directory.');
1679
+ }
1680
+
1681
+ const { kbToken } = await fetchKBJWT(kbId);
1682
+
1683
+ switch (subCommand) {
1684
+ case 'enable':
1685
+ return await postgresEnableAction(kbToken);
1686
+ case 'status':
1687
+ return await postgresStatusAction(kbToken);
1688
+ case 'connection':
1689
+ case 'conn':
1690
+ return await postgresConnectionAction(kbToken);
1691
+ case 'disable':
1692
+ return await postgresDisableAction(kbToken);
1693
+ default:
1694
+ console.log('Usage: openkbs postgres <command>');
1695
+ console.log('');
1696
+ console.log('Commands:');
1697
+ console.log(' enable Enable Postgres database for this KB');
1698
+ console.log(' status Show Postgres status and info');
1699
+ console.log(' connection Show connection string');
1700
+ console.log(' disable Disable Postgres (deletes database)');
1701
+ }
1702
+ }
1703
+
1704
+ async function postgresEnableAction(kbToken) {
1705
+ try {
1706
+ const region = findRegion();
1707
+ console.log(`Enabling Elastic Postgres in ${region}...`);
1708
+
1709
+ const response = await makePostRequest(KB_API_URL, {
1710
+ token: kbToken,
1711
+ action: 'enableElasticPostgres',
1712
+ region
1713
+ });
1714
+
1715
+ if (response.error) {
1716
+ return console.red('Error:', response.error);
1717
+ }
1718
+
1719
+ if (response.alreadyEnabled) {
1720
+ console.yellow('Postgres already enabled.');
1721
+ } else {
1722
+ console.green('Postgres enabled successfully!');
1723
+ }
1724
+
1725
+ console.log(` Host: ${response.host}`);
1726
+ console.log(` Port: ${response.port}`);
1727
+ console.log(` Database: ${response.dbName}`);
1728
+ console.log(` Region: ${response.region}`);
1729
+
1730
+ if (response.functionsUpdated > 0) {
1731
+ console.log(` Updated ${response.functionsUpdated} function(s) with DATABASE_URL`);
1732
+ }
1733
+ } catch (error) {
1734
+ console.red('Error enabling Postgres:', error.message);
1735
+ }
1736
+ }
1737
+
1738
+ async function postgresStatusAction(kbToken) {
1739
+ try {
1740
+ const response = await makePostRequest(KB_API_URL, {
1741
+ token: kbToken,
1742
+ action: 'getElasticPostgres'
1743
+ });
1744
+
1745
+ if (response.error) {
1746
+ return console.red('Error:', response.error);
1747
+ }
1748
+
1749
+ if (!response.enabled) {
1750
+ console.yellow('Postgres is not enabled.');
1751
+ console.log('Run: openkbs postgres enable');
1752
+ return;
1753
+ }
1754
+
1755
+ console.green('Postgres Status: Enabled');
1756
+ console.log(` Host: ${response.host}`);
1757
+ console.log(` Port: ${response.port}`);
1758
+ console.log(` Database: ${response.dbName}`);
1759
+ console.log(` Region: ${response.region}`);
1760
+ console.log(` Project: ${response.projectId}`);
1761
+ } catch (error) {
1762
+ console.red('Error getting Postgres status:', error.message);
1763
+ }
1764
+ }
1765
+
1766
+ async function postgresConnectionAction(kbToken) {
1767
+ try {
1768
+ const response = await makePostRequest(KB_API_URL, {
1769
+ token: kbToken,
1770
+ action: 'getElasticPostgresConnection'
1771
+ });
1772
+
1773
+ if (response.error) {
1774
+ return console.red('Error:', response.error);
1775
+ }
1776
+
1777
+ console.log('Connection String:');
1778
+ console.log(response.connectionString);
1779
+ } catch (error) {
1780
+ console.red('Error getting connection:', error.message);
1781
+ }
1782
+ }
1783
+
1784
+ async function postgresDisableAction(kbToken) {
1785
+ try {
1786
+ console.log('Disabling Elastic Postgres...');
1787
+ console.yellow('Warning: This will permanently delete the database and all data!');
1788
+
1789
+ const response = await makePostRequest(KB_API_URL, {
1790
+ token: kbToken,
1791
+ action: 'deleteElasticPostgres'
1792
+ });
1793
+
1794
+ if (response.error) {
1795
+ return console.red('Error:', response.error);
1796
+ }
1797
+
1798
+ console.green('Postgres disabled successfully.');
1799
+
1800
+ if (response.functionsUpdated > 0) {
1801
+ console.log(`Removed DATABASE_URL from ${response.functionsUpdated} function(s)`);
1802
+ }
1803
+ } catch (error) {
1804
+ console.red('Error disabling Postgres:', error.message);
1805
+ }
1806
+ }
1807
+
1808
+ // ===== Site Commands =====
1809
+
1810
+ async function siteAction(subCommand, args = []) {
1811
+ // Find kbId and site directory
1812
+ let kbId = findKbId();
1813
+ let siteDir = process.cwd();
1814
+
1815
+ // If no settings.json in current dir, check site/ subdirectory
1816
+ if (!fs.existsSync(path.join(process.cwd(), 'settings.json'))) {
1817
+ const siteDirPath = path.join(process.cwd(), 'site');
1818
+ const siteSettingsPath = path.join(siteDirPath, 'settings.json');
1819
+ if (fs.existsSync(siteSettingsPath)) {
1820
+ siteDir = siteDirPath;
1821
+ try {
1822
+ const settings = JSON.parse(fs.readFileSync(siteSettingsPath, 'utf8'));
1823
+ kbId = settings.kbId;
1824
+ } catch (e) {}
1825
+ }
1826
+ }
1827
+
1828
+ if (!kbId) {
1829
+ const localKBData = await fetchLocalKBData();
1830
+ kbId = localKBData?.kbId;
1831
+ }
1832
+
1833
+ if (!kbId) {
1834
+ return console.red('No KB found. Create settings.json with {"kbId": "..."} in current dir or site/ subdirectory.');
1835
+ }
1836
+
1837
+ const { kbToken } = await fetchKBJWT(kbId);
1838
+
1839
+ switch (subCommand) {
1840
+ case 'deploy':
1841
+ return await siteDeployAction(kbToken, kbId, siteDir, args);
1842
+ default:
1843
+ console.log('Site management commands:\n');
1844
+ console.log(' openkbs site deploy Upload all files to S3');
1845
+ console.log('\nRun from a folder containing settings.json with kbId, or from parent with site/ subdirectory');
1846
+ }
1847
+ }
1848
+
1849
+ async function siteDeployAction(kbToken, kbId, siteDir, args) {
1850
+
1851
+ // Walk directory and get all files (excluding settings.json and hidden files)
1852
+ const walkDir = async (dir, baseDir = dir) => {
1853
+ const files = [];
1854
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1855
+
1856
+ for (const entry of entries) {
1857
+ const fullPath = path.join(dir, entry.name);
1858
+ const relativePath = path.relative(baseDir, fullPath);
1859
+
1860
+ // Skip hidden files, settings.json, and node_modules
1861
+ if (entry.name.startsWith('.') ||
1862
+ entry.name === 'settings.json' ||
1863
+ entry.name === 'node_modules') {
1864
+ continue;
1865
+ }
1866
+
1867
+ if (entry.isDirectory()) {
1868
+ files.push(...await walkDir(fullPath, baseDir));
1869
+ } else {
1870
+ files.push(relativePath);
1871
+ }
1872
+ }
1873
+ return files;
1874
+ };
1875
+
1876
+ try {
1877
+ console.log(`Uploading site files for KB ${kbId}...`);
1878
+
1879
+ const files = await walkDir(siteDir);
1880
+
1881
+ if (files.length === 0) {
1882
+ return console.yellow('No files found to upload.');
1883
+ }
1884
+
1885
+ console.log(`Found ${files.length} files to upload.`);
1886
+
1887
+ let uploaded = 0;
1888
+ for (const file of files) {
1889
+ const filePath = path.join(siteDir, file);
1890
+ const fileContent = fs.readFileSync(filePath);
1891
+
1892
+ // Get presigned URL for 'files' namespace with correct Content-Type
1893
+ const contentType = getMimeType(file);
1894
+ const response = await makePostRequest(KB_API_URL, {
1895
+ token: kbToken,
1896
+ namespace: 'files',
1897
+ kbId,
1898
+ fileName: file,
1899
+ fileType: contentType,
1900
+ presignedOperation: 'putObject',
1901
+ action: 'createPresignedURL'
1902
+ });
1903
+
1904
+ if (response.error) {
1905
+ console.red(`Failed to get presigned URL for ${file}:`, response.error);
1906
+ continue;
1907
+ }
1908
+
1909
+ // Upload file with correct Content-Type
1910
+ await fetch(response, {
1911
+ method: 'PUT',
1912
+ body: fileContent,
1913
+ headers: { 'Content-Type': contentType }
1914
+ });
1915
+ uploaded++;
1916
+ console.log(`Uploaded: ${file} (${contentType})`);
1917
+ }
1918
+
1919
+ console.green(`\nUpload complete! ${uploaded}/${files.length} files uploaded.`);
1920
+ console.log(`Files accessible at: https://files.openkbs.com/${kbId}/`);
1921
+
1922
+ } catch (error) {
1923
+ console.red('Upload failed:', error.message);
1924
+ }
1925
+ }
1926
+
1927
+ // ===== Elastic Pulse Commands =====
1928
+
1929
+ async function pulseAction(subCommand, args = []) {
1930
+ // Find kbId from settings.json (current dir, app/, functions/, site/)
1931
+ let kbId = findKbId();
1932
+
1933
+ if (!kbId) {
1934
+ const localKBData = await fetchLocalKBData();
1935
+ kbId = localKBData?.kbId;
1936
+ }
1937
+
1938
+ if (!kbId) {
1939
+ return console.red('No KB found. Create settings.json with {"kbId": "..."} or run from a KB project directory.');
1940
+ }
1941
+
1942
+ const { kbToken } = await fetchKBJWT(kbId);
1943
+
1944
+ switch (subCommand) {
1945
+ case 'enable':
1946
+ return await pulseEnableAction(kbToken);
1947
+ case 'status':
1948
+ return await pulseStatusAction(kbToken);
1949
+ case 'disable':
1950
+ return await pulseDisableAction(kbToken);
1951
+ case 'channels':
1952
+ return await pulseChannelsAction(kbToken);
1953
+ case 'presence':
1954
+ return await pulsePresenceAction(kbToken, args[0]);
1955
+ case 'publish':
1956
+ case 'send':
1957
+ return await pulsePublishAction(kbToken, args[0], args.slice(1).join(' '));
1958
+ default:
1959
+ console.log('Usage: openkbs pulse <command>');
1960
+ console.log('');
1961
+ console.log('Commands:');
1962
+ console.log(' enable Enable Pulse (WebSocket) for this KB');
1963
+ console.log(' status Show Pulse status and endpoint');
1964
+ console.log(' disable Disable Pulse');
1965
+ console.log(' channels List active channels');
1966
+ console.log(' presence <channel> Show connected clients in channel');
1967
+ console.log(' publish <channel> <message> Send message to channel');
1968
+ }
1969
+ }
1970
+
1971
+ async function pulseEnableAction(kbToken) {
1972
+ try {
1973
+ console.log('Enabling Elastic Pulse...');
1974
+
1975
+ const response = await makePostRequest(KB_API_URL, {
1976
+ token: kbToken,
1977
+ action: 'enableElasticPulse'
1978
+ });
1979
+
1980
+ if (response.error) {
1981
+ return console.red('Error:', response.error);
1982
+ }
1983
+
1984
+ if (response.alreadyEnabled) {
1985
+ console.yellow('Pulse already enabled.');
1986
+ } else {
1987
+ console.green('Pulse enabled successfully!');
1988
+ }
1989
+
1990
+ console.log(` Endpoint: ${response.endpoint}`);
1991
+ console.log(` Region: ${response.region}`);
1992
+ } catch (error) {
1993
+ console.red('Error enabling Pulse:', error.message);
1994
+ }
1995
+ }
1996
+
1997
+ async function pulseStatusAction(kbToken) {
1998
+ try {
1999
+ const response = await makePostRequest(KB_API_URL, {
2000
+ token: kbToken,
2001
+ action: 'getElasticPulse'
2002
+ });
2003
+
2004
+ if (response.error) {
2005
+ return console.red('Error:', response.error);
2006
+ }
2007
+
2008
+ if (!response.enabled) {
2009
+ console.log('Elastic Pulse: disabled');
2010
+ console.log(' Use "openkbs pulse enable" to enable WebSocket messaging.');
2011
+ return;
2012
+ }
2013
+
2014
+ console.log('Elastic Pulse: enabled');
2015
+ console.log(` Endpoint: ${response.endpoint}`);
2016
+ console.log(` Region: ${response.region}`);
2017
+ } catch (error) {
2018
+ console.red('Error getting Pulse status:', error.message);
2019
+ }
2020
+ }
2021
+
2022
+ async function pulseDisableAction(kbToken) {
2023
+ try {
2024
+ console.log('Disabling Elastic Pulse...');
2025
+
2026
+ const response = await makePostRequest(KB_API_URL, {
2027
+ token: kbToken,
2028
+ action: 'disableElasticPulse'
2029
+ });
2030
+
2031
+ if (response.error) {
2032
+ return console.red('Error:', response.error);
2033
+ }
2034
+
2035
+ console.green('Pulse disabled successfully.');
2036
+ } catch (error) {
2037
+ console.red('Error disabling Pulse:', error.message);
2038
+ }
2039
+ }
2040
+
2041
+ async function pulseChannelsAction(kbToken) {
2042
+ try {
2043
+ const response = await makePostRequest(KB_API_URL, {
2044
+ token: kbToken,
2045
+ action: 'pulseChannels'
2046
+ });
2047
+
2048
+ if (response.error) {
2049
+ return console.red('Error:', response.error);
2050
+ }
2051
+
2052
+ if (!response.channels || response.channels.length === 0) {
2053
+ console.log('No active channels');
2054
+ return;
2055
+ }
2056
+
2057
+ console.log(`Active channels (${response.totalConnections} total connections):\n`);
2058
+ for (const ch of response.channels) {
2059
+ console.log(` ${ch.channel}: ${ch.count} connection(s)`);
2060
+ }
2061
+ } catch (error) {
2062
+ console.red('Error getting channels:', error.message);
2063
+ }
2064
+ }
2065
+
2066
+ async function pulsePresenceAction(kbToken, channel) {
2067
+ try {
2068
+ const response = await makePostRequest(KB_API_URL, {
2069
+ token: kbToken,
2070
+ action: 'pulsePresence',
2071
+ channel: channel || 'default'
2072
+ });
2073
+
2074
+ if (response.error) {
2075
+ return console.red('Error:', response.error);
2076
+ }
2077
+
2078
+ console.log(`Channel: ${response.channel}`);
2079
+ console.log(`Connected: ${response.count}`);
2080
+
2081
+ if (response.members && response.members.length > 0) {
2082
+ console.log('\nMembers:');
2083
+ for (const m of response.members) {
2084
+ const time = new Date(m.connectedAt).toISOString();
2085
+ console.log(` ${m.userId || 'anonymous'} (since ${time})`);
2086
+ }
2087
+ }
2088
+ } catch (error) {
2089
+ console.red('Error getting presence:', error.message);
2090
+ }
2091
+ }
2092
+
2093
+ async function pulsePublishAction(kbToken, channel, message) {
2094
+ if (!channel || !message) {
2095
+ return console.red('Usage: openkbs pulse publish <channel> <message>');
2096
+ }
2097
+
2098
+ try {
2099
+ const response = await makePostRequest(KB_API_URL, {
2100
+ token: kbToken,
2101
+ action: 'pulsePublish',
2102
+ channel,
2103
+ message
2104
+ });
2105
+
2106
+ if (response.error) {
2107
+ return console.red('Error:', response.error);
2108
+ }
2109
+
2110
+ console.green(`Message sent to ${response.sent} client(s) on channel "${response.channel}"`);
2111
+ } catch (error) {
2112
+ console.red('Error publishing message:', error.message);
2113
+ }
2114
+ }
2115
+
1183
2116
  module.exports = {
1184
2117
  signAction,
1185
2118
  loginAction,
@@ -1201,5 +2134,9 @@ module.exports = {
1201
2134
  updateCliAction,
1202
2135
  publishAction,
1203
2136
  unpublishAction,
1204
- fnAction
2137
+ fnAction,
2138
+ siteAction,
2139
+ storageAction,
2140
+ postgresAction,
2141
+ pulseAction
1205
2142
  };
package/src/index.js CHANGED
@@ -14,7 +14,11 @@ const {
14
14
  describeAction, deployAction, createByTemplateAction, initByTemplateAction,
15
15
  logoutAction, installFrontendPackageAction, modifyAction, downloadModifyAction,
16
16
  updateKnowledgeAction, updateCliAction, publishAction, unpublishAction,
17
- fnAction
17
+ fnAction,
18
+ siteAction,
19
+ storageAction,
20
+ postgresAction,
21
+ pulseAction
18
22
  } = require('./actions');
19
23
 
20
24
 
@@ -201,11 +205,12 @@ Examples:
201
205
  program
202
206
  .command('fn [subCommand] [args...]')
203
207
  .description('Manage Elastic Functions (serverless Lambda functions)')
208
+ .allowUnknownOption()
204
209
  .action((subCommand, args) => fnAction(subCommand, args))
205
210
  .addHelpText('after', `
206
211
  Examples:
207
212
  $ openkbs fn list List all functions
208
- $ openkbs fn deploy hello --region us-east-2 Deploy function from ./functions/hello/
213
+ $ openkbs fn deploy hello --region us-east-1 Deploy function from ./functions/hello/
209
214
  $ openkbs fn delete hello Delete a function
210
215
  $ openkbs fn logs hello View function logs
211
216
  $ openkbs fn env hello View environment variables
@@ -213,4 +218,59 @@ Examples:
213
218
  $ openkbs fn invoke hello '{"test": true}' Invoke a function
214
219
  `);
215
220
 
221
+ program
222
+ .command('site [subCommand] [args...]')
223
+ .description('Manage static site files for whitelabel domains')
224
+ .action((subCommand, args) => siteAction(subCommand, args))
225
+ .addHelpText('after', `
226
+ Examples:
227
+ $ openkbs site deploy Deploy all files to S3
228
+
229
+ Run from a directory containing settings.json with kbId.
230
+ Files are uploaded to the whitelabel domain's files bucket.
231
+ `);
232
+
233
+ program
234
+ .command('storage [subCommand] [args...]')
235
+ .description('Manage Elastic Storage (S3 buckets for persistent file storage)')
236
+ .action((subCommand, args) => storageAction(subCommand, args))
237
+ .addHelpText('after', `
238
+ Examples:
239
+ $ openkbs storage enable Enable storage for current KB
240
+ $ openkbs storage status Show storage status
241
+ $ openkbs storage ls [prefix] List objects in bucket
242
+ $ openkbs storage put <file> <key> Upload a file
243
+ $ openkbs storage get <key> <file> Download a file
244
+ $ openkbs storage rm <key> Delete an object
245
+ $ openkbs storage disable Disable storage (delete bucket)
246
+ $ openkbs storage cloudfront media Add storage to CloudFront at /media/*
247
+ $ openkbs storage cloudfront remove media Remove storage from CloudFront
248
+ `);
249
+
250
+ program
251
+ .command('postgres [subCommand]')
252
+ .description('Manage Elastic Postgres (Neon PostgreSQL database)')
253
+ .action((subCommand) => postgresAction(subCommand))
254
+ .addHelpText('after', `
255
+ Examples:
256
+ $ openkbs postgres enable Enable Postgres for current KB
257
+ $ openkbs postgres status Show Postgres status
258
+ $ openkbs postgres connection Show connection string
259
+ $ openkbs postgres disable Disable Postgres (delete database)
260
+ `);
261
+
262
+ program
263
+ .command('pulse [subCommand] [args...]')
264
+ .description('Manage Elastic Pulse (real-time WebSocket pub/sub)')
265
+ .action((subCommand, args) => pulseAction(subCommand, args))
266
+ .addHelpText('after', `
267
+ Examples:
268
+ $ openkbs pulse enable Enable Pulse for current KB
269
+ $ openkbs pulse status Show Pulse status and endpoint
270
+ $ openkbs pulse channels List active channels
271
+ $ openkbs pulse presence chat Show clients connected to 'chat' channel
272
+ $ openkbs pulse publish chat "Hello!" Send message to 'chat' channel
273
+ $ openkbs pulse disable Disable Pulse
274
+ `);
275
+
216
276
  program.parse(process.argv);
package/src/utils.js CHANGED
@@ -164,8 +164,11 @@ function makePostRequest(url, data) {
164
164
  resolve(data);
165
165
  } else {
166
166
  try {
167
- if (JSON.parse(body).error) {
168
- console.red(JSON.parse(body).error);
167
+ const parsed = JSON.parse(body);
168
+ if (parsed.error) {
169
+ console.red(parsed.error);
170
+ } else if (parsed.message) {
171
+ console.red(parsed.message);
169
172
  } else {
170
173
  console.red(`Invalid Request`);
171
174
  }
package/version.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.55",
3
- "releaseDate": "2025-12-28",
4
- "releaseNotes": "OpenKBS CLI version 0.0.55"
2
+ "version": "0.0.59",
3
+ "releaseDate": "2025-12-29",
4
+ "releaseNotes": "OpenKBS CLI version 0.0.59"
5
5
  }