spindb 0.38.1 → 0.40.0

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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/backup.js +13 -2
  3. package/dist/cli/commands/backup.js.map +1 -1
  4. package/dist/cli/commands/clone.js +13 -1
  5. package/dist/cli/commands/clone.js.map +1 -1
  6. package/dist/cli/commands/connect.js +112 -40
  7. package/dist/cli/commands/connect.js.map +1 -1
  8. package/dist/cli/commands/delete.js +27 -0
  9. package/dist/cli/commands/delete.js.map +1 -1
  10. package/dist/cli/commands/engines.js +1 -1
  11. package/dist/cli/commands/engines.js.map +1 -1
  12. package/dist/cli/commands/export.js +12 -1
  13. package/dist/cli/commands/export.js.map +1 -1
  14. package/dist/cli/commands/info.js +54 -12
  15. package/dist/cli/commands/info.js.map +1 -1
  16. package/dist/cli/commands/link.js +215 -0
  17. package/dist/cli/commands/link.js.map +1 -0
  18. package/dist/cli/commands/list.js +28 -7
  19. package/dist/cli/commands/list.js.map +1 -1
  20. package/dist/cli/commands/logs.js +6 -0
  21. package/dist/cli/commands/logs.js.map +1 -1
  22. package/dist/cli/commands/menu/container-handlers.js +260 -32
  23. package/dist/cli/commands/menu/container-handlers.js.map +1 -1
  24. package/dist/cli/commands/menu/index.js +20 -4
  25. package/dist/cli/commands/menu/index.js.map +1 -1
  26. package/dist/cli/commands/menu/shell-handlers.js +206 -75
  27. package/dist/cli/commands/menu/shell-handlers.js.map +1 -1
  28. package/dist/cli/commands/query.js +35 -4
  29. package/dist/cli/commands/query.js.map +1 -1
  30. package/dist/cli/commands/restore.js +12 -1
  31. package/dist/cli/commands/restore.js.map +1 -1
  32. package/dist/cli/commands/run.js +6 -1
  33. package/dist/cli/commands/run.js.map +1 -1
  34. package/dist/cli/commands/start.js +17 -2
  35. package/dist/cli/commands/start.js.map +1 -1
  36. package/dist/cli/commands/stop.js +16 -1
  37. package/dist/cli/commands/stop.js.map +1 -1
  38. package/dist/cli/commands/url.js +59 -15
  39. package/dist/cli/commands/url.js.map +1 -1
  40. package/dist/cli/index.js +2 -0
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/config/version.js +1 -1
  43. package/dist/core/container-manager.js +15 -1
  44. package/dist/core/container-manager.js.map +1 -1
  45. package/dist/core/remote-container.js +228 -0
  46. package/dist/core/remote-container.js.map +1 -0
  47. package/dist/engines/ferretdb/index.js +29 -10
  48. package/dist/engines/ferretdb/index.js.map +1 -1
  49. package/dist/engines/ferretdb/restore.js.map +1 -1
  50. package/dist/engines/mariadb/index.js +10 -2
  51. package/dist/engines/mariadb/index.js.map +1 -1
  52. package/dist/engines/mongodb/index.js +25 -3
  53. package/dist/engines/mongodb/index.js.map +1 -1
  54. package/dist/engines/mysql/index.js +10 -2
  55. package/dist/engines/mysql/index.js.map +1 -1
  56. package/dist/engines/postgresql/index.js +14 -2
  57. package/dist/engines/postgresql/index.js.map +1 -1
  58. package/dist/engines/redis/index.js +8 -1
  59. package/dist/engines/redis/index.js.map +1 -1
  60. package/dist/engines/valkey/index.js +8 -1
  61. package/dist/engines/valkey/index.js.map +1 -1
  62. package/dist/types/index.js +6 -0
  63. package/dist/types/index.js.map +1 -1
  64. package/package.json +3 -2
@@ -14,6 +14,9 @@ import { configManager } from '../../../core/config-manager.js';
14
14
  import { getPgwebStatus, stopPgweb, PGWEB_VERSION, } from '../../../core/pgweb-utils.js';
15
15
  import { DBLAB_ENGINES, DBLAB_VERSION, getDblabArgs, getDblabPlatformSuffix, } from '../../../core/dblab-utils.js';
16
16
  import { getEngine } from '../../../engines/index.js';
17
+ import { isRemoteContainer } from '../../../types/index.js';
18
+ import { loadCredentials } from '../../../core/credential-manager.js';
19
+ import { redactConnectionString, parseConnectionString, } from '../../../core/remote-container.js';
17
20
  import { createSpinner } from '../../ui/spinner.js';
18
21
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme.js';
19
22
  import { logDebug } from '../../../core/error-handler.js';
@@ -50,17 +53,28 @@ export async function handleCopyConnectionString(containerName, database) {
50
53
  return;
51
54
  }
52
55
  const engine = getEngine(config.engine);
53
- // Use provided database or fall back to container's default
54
- const connectionString = engine.getConnectionString(config, database);
56
+ // Remote containers: use stored connection string (full from credentials)
57
+ let connectionString;
58
+ let displayString;
59
+ if (isRemoteContainer(config)) {
60
+ const creds = await loadCredentials(config.name, config.engine, 'remote');
61
+ connectionString =
62
+ creds?.connectionString ?? config.remote?.connectionString ?? '';
63
+ displayString = redactConnectionString(connectionString);
64
+ }
65
+ else {
66
+ connectionString = engine.getConnectionString(config, database);
67
+ displayString = connectionString;
68
+ }
55
69
  const copied = await platformService.copyToClipboard(connectionString);
56
70
  console.log();
57
71
  if (copied) {
58
72
  console.log(uiSuccess('Connection string copied to clipboard'));
59
- console.log(chalk.gray(` ${connectionString}`));
73
+ console.log(chalk.gray(` ${displayString}`));
60
74
  }
61
75
  else {
62
76
  console.log(uiWarning('Could not copy to clipboard. Connection string:'));
63
- console.log(chalk.cyan(` ${connectionString}`));
77
+ console.log(chalk.cyan(` ${displayString}`));
64
78
  }
65
79
  console.log();
66
80
  await escapeablePrompt([
@@ -78,9 +92,19 @@ export async function handleOpenShell(containerName, database) {
78
92
  return;
79
93
  }
80
94
  const engine = getEngine(config.engine);
95
+ const isRemote = isRemoteContainer(config);
81
96
  // Use provided database or fall back to container's default
82
97
  const activeDatabase = database || config.database;
83
- const connectionString = engine.getConnectionString(config, activeDatabase);
98
+ // For remote containers, use the stored remote connection string
99
+ let connectionString;
100
+ if (isRemote) {
101
+ const creds = await loadCredentials(config.name, config.engine, 'remote');
102
+ connectionString =
103
+ creds?.connectionString ?? config.remote?.connectionString ?? '';
104
+ }
105
+ else {
106
+ connectionString = engine.getConnectionString(config, activeDatabase);
107
+ }
84
108
  const shellCheckSpinner = createSpinner('Checking available shells...');
85
109
  shellCheckSpinner.start();
86
110
  const [usqlInstalled, pgcliInstalled, mycliInstalled, litecliInstalled, iredisInstalled,] = await Promise.all([
@@ -93,6 +117,16 @@ export async function handleOpenShell(containerName, database) {
93
117
  shellCheckSpinner.stop();
94
118
  // Clear the spinner line
95
119
  process.stdout.write('\x1b[1A\x1b[2K');
120
+ // REST API engines (no CLI shell) can't be used remotely via console
121
+ if (isRemote &&
122
+ ['qdrant', 'meilisearch', 'influxdb', 'weaviate', 'couchdb'].includes(config.engine)) {
123
+ console.log();
124
+ console.log(uiInfo('Console is not available for linked remote REST API databases.'));
125
+ console.log(chalk.gray(" Use your provider's web dashboard or API tools directly."));
126
+ console.log();
127
+ await pressEnterToContinue();
128
+ return;
129
+ }
96
130
  // Engine-specific shell names
97
131
  let defaultShellName;
98
132
  let engineSpecificCli;
@@ -364,7 +398,8 @@ export async function handleOpenShell(containerName, database) {
364
398
  }
365
399
  }
366
400
  // dblab visual TUI (supports PostgreSQL, MySQL, MariaDB, CockroachDB, SQLite, QuestDB)
367
- if (DBLAB_ENGINES.has(config.engine)) {
401
+ // Not available for remote containers (hardcodes local connection)
402
+ if (DBLAB_ENGINES.has(config.engine) && !isRemote) {
368
403
  const dblabPath = await configManager.getBinaryPath('dblab');
369
404
  if (dblabPath) {
370
405
  choices.push({
@@ -379,8 +414,8 @@ export async function handleOpenShell(containerName, database) {
379
414
  });
380
415
  }
381
416
  }
382
- // Web Panel section for engines with browser-based UIs
383
- if (config.engine === 'clickhouse') {
417
+ // Web Panel section for engines with browser-based UIs (local only)
418
+ if (config.engine === 'clickhouse' && !isRemote) {
384
419
  const httpPort = config.port + 1;
385
420
  choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)));
386
421
  choices.push({
@@ -388,7 +423,7 @@ export async function handleOpenShell(containerName, database) {
388
423
  value: 'browser',
389
424
  });
390
425
  }
391
- if (config.engine === 'questdb') {
426
+ if (config.engine === 'questdb' && !isRemote) {
392
427
  const httpPort = config.port + 188;
393
428
  choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)));
394
429
  choices.push({
@@ -396,16 +431,17 @@ export async function handleOpenShell(containerName, database) {
396
431
  value: 'browser',
397
432
  });
398
433
  }
399
- if (config.engine === 'duckdb') {
434
+ if (config.engine === 'duckdb' && !isRemote) {
400
435
  choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)));
401
436
  choices.push({
402
437
  name: `◎ Open Web UI (built-in, port 4213)`,
403
438
  value: 'duckdb-ui',
404
439
  });
405
440
  }
406
- if (config.engine === 'postgresql' ||
407
- config.engine === 'cockroachdb' ||
408
- config.engine === 'ferretdb') {
441
+ if (!isRemote &&
442
+ (config.engine === 'postgresql' ||
443
+ config.engine === 'cockroachdb' ||
444
+ config.engine === 'ferretdb')) {
409
445
  choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)));
410
446
  const pgwebPath = await configManager.getBinaryPath('pgweb');
411
447
  if (pgwebPath) {
@@ -1179,6 +1215,24 @@ async function launchPgweb(containerName, config, database) {
1179
1215
  async function launchShell(containerName, config, connectionString, shellType, database) {
1180
1216
  console.log(uiInfo(`Connecting to ${containerName}...`));
1181
1217
  console.log();
1218
+ const isRemote = isRemoteContainer(config);
1219
+ // Parse remote connection string for host/port/user/password
1220
+ let rHost = '127.0.0.1';
1221
+ let rPort = config.port;
1222
+ let rUser = '';
1223
+ let rPass = '';
1224
+ if (isRemote) {
1225
+ try {
1226
+ const parsed = parseConnectionString(connectionString);
1227
+ rHost = parsed.host || config.remote?.host || '127.0.0.1';
1228
+ rPort = parsed.port || config.port;
1229
+ rUser = parsed.username || '';
1230
+ rPass = parsed.password || '';
1231
+ }
1232
+ catch {
1233
+ /* use defaults */
1234
+ }
1235
+ }
1182
1236
  let shellCmd;
1183
1237
  let shellArgs;
1184
1238
  let installHint;
@@ -1190,17 +1244,22 @@ async function launchShell(containerName, config, connectionString, shellType, d
1190
1244
  installHint = 'brew install pgcli';
1191
1245
  }
1192
1246
  else if (shellType === 'mycli') {
1193
- // mycli: mycli -h host -P port -u user database
1247
+ // mycli accepts connection strings directly
1194
1248
  shellCmd = 'mycli';
1195
- shellArgs = [
1196
- '-h',
1197
- '127.0.0.1',
1198
- '-P',
1199
- String(config.port),
1200
- '-u',
1201
- 'root',
1202
- database,
1203
- ];
1249
+ if (isRemote) {
1250
+ shellArgs = [connectionString];
1251
+ }
1252
+ else {
1253
+ shellArgs = [
1254
+ '-h',
1255
+ '127.0.0.1',
1256
+ '-P',
1257
+ String(config.port),
1258
+ '-u',
1259
+ 'root',
1260
+ database,
1261
+ ];
1262
+ }
1204
1263
  installHint = 'brew install mycli';
1205
1264
  }
1206
1265
  else if (shellType === 'litecli') {
@@ -1232,30 +1291,46 @@ async function launchShell(containerName, config, connectionString, shellType, d
1232
1291
  // MySQL uses downloaded binaries - get the actual path
1233
1292
  const mysqlPath = await configManager.getBinaryPath('mysql');
1234
1293
  shellCmd = mysqlPath || 'mysql';
1235
- shellArgs = [
1236
- '-u',
1237
- 'root',
1238
- '-h',
1239
- '127.0.0.1',
1240
- '-P',
1241
- String(config.port),
1242
- database,
1243
- ];
1294
+ if (isRemote) {
1295
+ shellArgs = ['-h', rHost, '-P', String(rPort), '-u', rUser || 'root'];
1296
+ if (rPass)
1297
+ shellArgs.push(`-p${rPass}`);
1298
+ shellArgs.push(database);
1299
+ }
1300
+ else {
1301
+ shellArgs = [
1302
+ '-u',
1303
+ 'root',
1304
+ '-h',
1305
+ '127.0.0.1',
1306
+ '-P',
1307
+ String(config.port),
1308
+ database,
1309
+ ];
1310
+ }
1244
1311
  installHint = 'spindb engines download mysql';
1245
1312
  }
1246
1313
  else if (config.engine === 'mariadb') {
1247
1314
  // MariaDB uses downloaded binaries, not system PATH - get the actual path
1248
1315
  const mariadbPath = await configManager.getBinaryPath('mariadb');
1249
1316
  shellCmd = mariadbPath || 'mariadb';
1250
- shellArgs = [
1251
- '-u',
1252
- 'root',
1253
- '-h',
1254
- '127.0.0.1',
1255
- '-P',
1256
- String(config.port),
1257
- database,
1258
- ];
1317
+ if (isRemote) {
1318
+ shellArgs = ['-h', rHost, '-P', String(rPort), '-u', rUser || 'root'];
1319
+ if (rPass)
1320
+ shellArgs.push(`-p${rPass}`);
1321
+ shellArgs.push(database);
1322
+ }
1323
+ else {
1324
+ shellArgs = [
1325
+ '-u',
1326
+ 'root',
1327
+ '-h',
1328
+ '127.0.0.1',
1329
+ '-P',
1330
+ String(config.port),
1331
+ database,
1332
+ ];
1333
+ }
1259
1334
  installHint = 'spindb engines download mariadb';
1260
1335
  }
1261
1336
  else if (config.engine === 'mongodb' || config.engine === 'ferretdb') {
@@ -1266,20 +1341,47 @@ async function launchShell(containerName, config, connectionString, shellType, d
1266
1341
  else if (shellType === 'iredis') {
1267
1342
  // iredis: enhanced Redis CLI
1268
1343
  shellCmd = 'iredis';
1269
- shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1344
+ if (isRemote) {
1345
+ shellArgs = ['-h', rHost, '-p', String(rPort)];
1346
+ if (rPass)
1347
+ shellArgs.push('-a', rPass);
1348
+ if (database)
1349
+ shellArgs.push('-n', database);
1350
+ }
1351
+ else {
1352
+ shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1353
+ }
1270
1354
  installHint = 'brew install iredis';
1271
1355
  }
1272
1356
  else if (config.engine === 'redis') {
1273
1357
  // Default Redis shell
1274
1358
  shellCmd = 'redis-cli';
1275
- shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1359
+ if (isRemote) {
1360
+ shellArgs = ['-h', rHost, '-p', String(rPort)];
1361
+ if (rPass)
1362
+ shellArgs.push('-a', rPass);
1363
+ if (database)
1364
+ shellArgs.push('-n', database);
1365
+ }
1366
+ else {
1367
+ shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1368
+ }
1276
1369
  installHint = 'brew install redis';
1277
1370
  }
1278
1371
  else if (config.engine === 'valkey') {
1279
1372
  // Default Valkey shell
1280
1373
  const valkeyCliPath = await configManager.getBinaryPath('valkey-cli');
1281
1374
  shellCmd = valkeyCliPath || 'valkey-cli';
1282
- shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1375
+ if (isRemote) {
1376
+ shellArgs = ['-h', rHost, '-p', String(rPort)];
1377
+ if (rPass)
1378
+ shellArgs.push('-a', rPass);
1379
+ if (database)
1380
+ shellArgs.push('-n', database);
1381
+ }
1382
+ else {
1383
+ shellArgs = ['-h', '127.0.0.1', '-p', String(config.port), '-n', database];
1384
+ }
1283
1385
  installHint = 'spindb engines download valkey';
1284
1386
  }
1285
1387
  else if (config.engine === 'clickhouse') {
@@ -1289,12 +1391,16 @@ async function launchShell(containerName, config, connectionString, shellType, d
1289
1391
  shellArgs = [
1290
1392
  'client',
1291
1393
  '--host',
1292
- '127.0.0.1',
1394
+ isRemote ? rHost : '127.0.0.1',
1293
1395
  '--port',
1294
- String(config.port),
1396
+ String(isRemote ? rPort : config.port),
1295
1397
  '--database',
1296
1398
  database,
1297
1399
  ];
1400
+ if (isRemote && rUser)
1401
+ shellArgs.push('--user', rUser);
1402
+ if (isRemote && rPass)
1403
+ shellArgs.push('--password', rPass);
1298
1404
  installHint = 'spindb engines download clickhouse';
1299
1405
  }
1300
1406
  else if (config.engine === 'qdrant') {
@@ -1447,19 +1553,36 @@ async function launchShell(containerName, config, connectionString, shellType, d
1447
1553
  .catch(() => 'surreal');
1448
1554
  const namespace = config.name.replace(/-/g, '_');
1449
1555
  shellCmd = surrealPath;
1450
- shellArgs = [
1451
- 'sql',
1452
- '--endpoint',
1453
- `ws://127.0.0.1:${config.port}`,
1454
- '--namespace',
1455
- namespace,
1456
- '--database',
1457
- database || 'default',
1458
- '--username',
1459
- 'root',
1460
- '--password',
1461
- 'root',
1462
- ];
1556
+ if (isRemote) {
1557
+ shellArgs = [
1558
+ 'sql',
1559
+ '--endpoint',
1560
+ `ws://${rHost}:${rPort}`,
1561
+ '--namespace',
1562
+ namespace,
1563
+ '--database',
1564
+ database || 'default',
1565
+ ];
1566
+ if (rUser)
1567
+ shellArgs.push('--username', rUser);
1568
+ if (rPass)
1569
+ shellArgs.push('--password', rPass);
1570
+ }
1571
+ else {
1572
+ shellArgs = [
1573
+ 'sql',
1574
+ '--endpoint',
1575
+ `ws://127.0.0.1:${config.port}`,
1576
+ '--namespace',
1577
+ namespace,
1578
+ '--database',
1579
+ database || 'default',
1580
+ '--username',
1581
+ 'root',
1582
+ '--password',
1583
+ 'root',
1584
+ ];
1585
+ }
1463
1586
  installHint = 'spindb engines download surrealdb';
1464
1587
  // SurrealDB writes history.txt to cwd - use container directory
1465
1588
  spawnCwd = join(paths.containers, 'surrealdb', config.name);
@@ -1471,24 +1594,34 @@ async function launchShell(containerName, config, connectionString, shellType, d
1471
1594
  .getCockroachPath(config.version)
1472
1595
  .catch(() => 'cockroach');
1473
1596
  shellCmd = cockroachPath;
1474
- shellArgs = [
1475
- 'sql',
1476
- '--insecure',
1477
- '--host',
1478
- `127.0.0.1:${config.port}`,
1479
- '--database',
1480
- database,
1481
- ];
1597
+ if (isRemote) {
1598
+ // Use --url for remote connections (supports full connection strings)
1599
+ shellArgs = ['sql', '--url', connectionString];
1600
+ }
1601
+ else {
1602
+ shellArgs = [
1603
+ 'sql',
1604
+ '--insecure',
1605
+ '--host',
1606
+ `127.0.0.1:${config.port}`,
1607
+ '--database',
1608
+ database,
1609
+ ];
1610
+ }
1482
1611
  installHint = 'spindb engines download cockroachdb';
1483
1612
  }
1484
1613
  else if (config.engine === 'questdb') {
1485
1614
  // QuestDB uses PostgreSQL wire protocol on port 8812
1486
- // Default credentials: admin/quest
1487
1615
  shellCmd = 'psql';
1488
- const db = database || 'qdb';
1489
- // QuestDB connection string with explicit password
1490
- const questDbConnStr = `postgresql://admin:quest@127.0.0.1:${config.port}/${db}`;
1491
- shellArgs = [questDbConnStr];
1616
+ if (isRemote) {
1617
+ shellArgs = [connectionString];
1618
+ }
1619
+ else {
1620
+ // Default credentials: admin/quest
1621
+ const db = database || 'qdb';
1622
+ const questDbConnStr = `postgresql://admin:quest@127.0.0.1:${config.port}/${db}`;
1623
+ shellArgs = [questDbConnStr];
1624
+ }
1492
1625
  installHint = 'brew install libpq && brew link --force libpq';
1493
1626
  }
1494
1627
  else if (config.engine === 'typedb') {
@@ -1510,8 +1643,6 @@ async function launchShell(containerName, config, connectionString, shellType, d
1510
1643
  }
1511
1644
  else if (config.engine === 'tigerbeetle') {
1512
1645
  // TigerBeetle uses tigerbeetle repl command
1513
- // Cluster ID 0 is the default for local single-node development.
1514
- // TigerBeetle format/start also use cluster 0 (see engines/tigerbeetle/index.ts).
1515
1646
  const clusterId = 0;
1516
1647
  const engine = getEngine(config.engine);
1517
1648
  const tigerbeetlePath = await engine
@@ -1521,7 +1652,7 @@ async function launchShell(containerName, config, connectionString, shellType, d
1521
1652
  shellArgs = [
1522
1653
  'repl',
1523
1654
  `--cluster=${clusterId}`,
1524
- `--addresses=127.0.0.1:${config.port}`,
1655
+ `--addresses=${isRemote ? rHost : '127.0.0.1'}:${isRemote ? rPort : config.port}`,
1525
1656
  ];
1526
1657
  installHint = 'spindb engines download tigerbeetle';
1527
1658
  }