spindb 0.40.0 → 0.42.4

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 (41) hide show
  1. package/README.md +6 -1
  2. package/dist/cli/commands/databases.js +647 -12
  3. package/dist/cli/commands/databases.js.map +1 -1
  4. package/dist/cli/commands/menu/container-handlers.js +788 -175
  5. package/dist/cli/commands/menu/container-handlers.js.map +1 -1
  6. package/dist/cli/commands/query.js +3 -2
  7. package/dist/cli/commands/query.js.map +1 -1
  8. package/dist/config/paths.js +2 -0
  9. package/dist/config/paths.js.map +1 -1
  10. package/dist/config/version.js +1 -1
  11. package/dist/core/container-manager.js +21 -0
  12. package/dist/core/container-manager.js.map +1 -1
  13. package/dist/core/database-capabilities.js +156 -0
  14. package/dist/core/database-capabilities.js.map +1 -0
  15. package/dist/engines/base-engine.js +8 -0
  16. package/dist/engines/base-engine.js.map +1 -1
  17. package/dist/engines/clickhouse/index.js +45 -0
  18. package/dist/engines/clickhouse/index.js.map +1 -1
  19. package/dist/engines/cockroachdb/index.js +45 -0
  20. package/dist/engines/cockroachdb/index.js.map +1 -1
  21. package/dist/engines/ferretdb/index.js +4 -3
  22. package/dist/engines/ferretdb/index.js.map +1 -1
  23. package/dist/engines/mariadb/index.js +12 -13
  24. package/dist/engines/mariadb/index.js.map +1 -1
  25. package/dist/engines/meilisearch/index.js +50 -1
  26. package/dist/engines/meilisearch/index.js.map +1 -1
  27. package/dist/engines/mongodb/index.js +46 -16
  28. package/dist/engines/mongodb/index.js.map +1 -1
  29. package/dist/engines/mysql/index.js +12 -13
  30. package/dist/engines/mysql/index.js.map +1 -1
  31. package/dist/engines/postgresql/index.js +54 -5
  32. package/dist/engines/postgresql/index.js.map +1 -1
  33. package/dist/engines/qdrant/index.js +2 -2
  34. package/dist/engines/qdrant/index.js.map +1 -1
  35. package/dist/engines/redis/index.js +15 -3
  36. package/dist/engines/redis/index.js.map +1 -1
  37. package/dist/engines/valkey/index.js +16 -4
  38. package/dist/engines/valkey/index.js.map +1 -1
  39. package/dist/engines/weaviate/index.js +1 -1
  40. package/dist/engines/weaviate/index.js.map +1 -1
  41. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { existsSync, renameSync, statSync, mkdirSync, copyFileSync, unlinkSync,
4
4
  import { stat, mkdir, rm } from 'fs/promises';
5
5
  import { dirname, basename, join, resolve } from 'path';
6
6
  import { homedir } from 'os';
7
- import { containerManager } from '../../../core/container-manager.js';
7
+ import { containerManager, updateRenameTracking, } from '../../../core/container-manager.js';
8
8
  import { getMissingDependencies } from '../../../core/dependency-manager.js';
9
9
  import { platformService } from '../../../core/platform-service.js';
10
10
  import { portManager } from '../../../core/port-manager.js';
@@ -17,7 +17,7 @@ import { defaults } from '../../../config/defaults.js';
17
17
  import { getEngineConfig } from '../../../config/engines-registry.js';
18
18
  import { getPageSize } from '../../constants.js';
19
19
  import { paths } from '../../../config/paths.js';
20
- import { promptContainerName, promptContainerSelect, promptInstallDependencies, promptConfirm, promptEngine, promptVersion, promptPort, promptDatabaseName, promptFileDatabasePath, escapeablePrompt, filterableListPrompt, BACK_VALUE, MAIN_MENU_VALUE, TOGGLE_PREFIX, } from '../../ui/prompts.js';
20
+ import { promptContainerName, promptContainerSelect, promptInstallDependencies, promptConfirm, promptEngine, promptVersion, promptPort, promptDatabaseName, promptFileDatabasePath, EscapeError, escapeablePrompt, filterableListPrompt, BACK_VALUE, MAIN_MENU_VALUE, TOGGLE_PREFIX, } from '../../ui/prompts.js';
21
21
  import { getEngineDefaults } from '../../../config/defaults.js';
22
22
  import { createSpinner } from '../../ui/spinner.js';
23
23
  import { header, uiSuccess, uiError, uiWarning, uiInfo, connectionBox, formatBytes, box, } from '../../ui/theme.js';
@@ -29,11 +29,20 @@ import { UnsupportedOperationError, isValidUsername, logDebug, } from '../../../
29
29
  import { handleRunSql, handleViewLogs } from './sql-handlers.js';
30
30
  import { handleBackupForContainer, handleRestoreForContainer, } from './backup-handlers.js';
31
31
  import { exportToDocker, getExportBackupPath, dockerExportExists, getDockerConnectionString, } from '../../../core/docker-exporter.js';
32
- import { getDefaultFormat } from '../../../config/backup-formats.js';
32
+ import { getDefaultFormat, getBackupExtension, } from '../../../config/backup-formats.js';
33
33
  import { parseConnectionString, detectEngineFromConnectionString, detectProvider, isLocalhost, generateRemoteContainerName, redactConnectionString, buildRemoteConfig, getDefaultPortForEngine, } from '../../../core/remote-container.js';
34
34
  import { Engine, isFileBasedEngine, isRemoteContainer } from '../../../types/index.js';
35
+ import { canCreateDatabase, canDropDatabase, canRenameDatabase, getDatabaseCapabilities, } from '../../../core/database-capabilities.js';
35
36
  import { pressEnterToContinue } from './shared.js';
36
37
  import { getEngineIcon } from '../../constants.js';
38
+ /** Helper for disabled menu items (hint shown in separator, not on each item) */
39
+ function disabledItem(icon, label) {
40
+ return {
41
+ name: chalk.gray(`${icon} ${label}`),
42
+ value: '_disabled_',
43
+ disabled: '', // Empty string hides the "(Disabled)" text
44
+ };
45
+ }
37
46
  export async function handleCreate() {
38
47
  console.log();
39
48
  console.log(header('Create New Database Container'));
@@ -674,7 +683,7 @@ export async function handleList(showMainMenu, options) {
674
683
  }
675
684
  await showContainerSubmenu(selectedContainer, showMainMenu);
676
685
  }
677
- export async function showContainerSubmenu(containerName, showMainMenu, selectedDatabase) {
686
+ export async function showContainerSubmenu(containerName, showMainMenu) {
678
687
  const config = await containerManager.getConfig(containerName);
679
688
  if (!config) {
680
689
  console.error(uiError(`Container "${containerName}" not found`));
@@ -708,8 +717,8 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
708
717
  }
709
718
  // Get list of databases in this container
710
719
  const databases = config.databases || [config.database];
711
- // Auto-select: use provided selection, or default to primary database from config
712
- const activeDatabase = selectedDatabase || config.database;
720
+ // Active database is always the default from config
721
+ const activeDatabase = config.database;
713
722
  // Header shows icon + container → database
714
723
  const engineIcon = getEngineIcon(config.engine);
715
724
  const headerText = `${engineIcon} ${containerName} ${chalk.gray('→')} ${activeDatabase}`;
@@ -719,14 +728,6 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
719
728
  console.log();
720
729
  // Build action choices based on engine type
721
730
  const actionChoices = [];
722
- // Helper for disabled menu items (hint shown in separator, not on each item)
723
- function disabledItem(icon, label) {
724
- return {
725
- name: chalk.gray(`${icon} ${label}`),
726
- value: '_disabled_',
727
- disabled: '', // Empty string hides the "(Disabled)" text
728
- };
729
- }
730
731
  // Remote containers get a simplified action set
731
732
  if (isRemote) {
732
733
  actionChoices.push(new inquirer.Separator(chalk.gray(`── Linked ──`)));
@@ -767,11 +768,11 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
767
768
  switch (action) {
768
769
  case 'shell':
769
770
  await handleOpenShell(containerName, activeDatabase);
770
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
771
+ await showContainerSubmenu(containerName, showMainMenu);
771
772
  return;
772
773
  case 'copy':
773
774
  await handleCopyConnectionString(containerName, activeDatabase);
774
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
775
+ await showContainerSubmenu(containerName, showMainMenu);
775
776
  return;
776
777
  case 'delete':
777
778
  await handleDelete(containerName);
@@ -785,7 +786,6 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
785
786
  return;
786
787
  }
787
788
  // Determine if database-specific actions can be performed
788
- // Requires: database selected + (running for server DBs OR file exists for file-based DBs)
789
789
  const containerReady = isFileBasedDB ? existsSync(config.database) : isRunning;
790
790
  const hasMultipleDatabases = databases.length > 1;
791
791
  const canDoDbAction = !!activeDatabase && containerReady;
@@ -794,10 +794,6 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
794
794
  if (!containerReady) {
795
795
  return isFileBasedDB ? 'Database file missing' : 'Start container first';
796
796
  }
797
- if (!activeDatabase && hasMultipleDatabases) {
798
- return 'Select database first';
799
- }
800
- // Show positive state when actions are available
801
797
  return isFileBasedDB ? 'Available' : 'Running';
802
798
  }
803
799
  // Label for management section separator - shows state or required action
@@ -843,61 +839,100 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
843
839
  value: 'logs',
844
840
  });
845
841
  }
846
- // Database selection - show current selection or prompt to select
847
- // Only show if there are multiple databases
848
- if (hasMultipleDatabases) {
849
- const dbIndex = activeDatabase ? databases.indexOf(activeDatabase) + 1 : 0;
850
- const dbLabel = activeDatabase
851
- ? `${chalk.cyan('◉')} Set database ${chalk.gray('|')} Current: ${chalk.white(activeDatabase)} ${chalk.gray(`(${dbIndex} of ${databases.length})`)}`
852
- : `${chalk.yellow('◉')} Set database`;
853
- actionChoices.push({
854
- name: dbLabel,
855
- value: 'select-database',
856
- });
857
- }
858
842
  // ─────────────────────────────────────────────────────────────────────────────
859
843
  // SECTION 2: Data Operations
860
844
  // Separator shows current state or required action
861
845
  // ─────────────────────────────────────────────────────────────────────────────
862
846
  const dataSectionLabel = getDataSectionLabel();
863
847
  actionChoices.push(new inquirer.Separator(chalk.gray(`── ${dataSectionLabel} ──`)));
864
- // Open console - requires database selection for multi-db containers
865
- actionChoices.push(canDoDbAction
866
- ? { name: `${chalk.blue('>')} Open console`, value: 'shell' }
867
- : disabledItem('>', 'Open console'));
868
- // Run script file - requires database selection for multi-db containers
869
- // Label comes from engines.json scriptFileLabel; null means no script support (REST API engines)
870
- const engineConfig = await getEngineConfig(config.engine);
871
- if (engineConfig.scriptFileLabel) {
872
- const runScriptLabel = engineConfig.scriptFileLabel;
873
- actionChoices.push(canDoDbAction
874
- ? { name: `${chalk.yellow('▷')} ${runScriptLabel}`, value: 'run-sql' }
875
- : disabledItem('▷', runScriptLabel));
876
- }
877
- // Copy connection string - requires database selection for multi-db containers
878
- actionChoices.push(canDoDbAction
879
- ? { name: `${chalk.green('⎘')} Copy connection string`, value: 'copy' }
880
- : disabledItem('⎘', 'Copy connection string'));
881
- // Create user - only for engines that override createUser from BaseEngine
882
- const engine = getEngine(config.engine);
883
- const supportsUsers = engine.createUser !== BaseEngine.prototype.createUser;
884
- if (supportsUsers) {
848
+ if (hasMultipleDatabases) {
849
+ // Multi-db: show "Databases (N)" entry — per-db actions are in the databases submenu
885
850
  actionChoices.push(containerReady
886
851
  ? {
887
- name: `${chalk.yellow('+')} Create user`,
888
- value: 'create_user',
852
+ name: `${chalk.cyan('')} Databases (${databases.length})`,
853
+ value: 'databases',
889
854
  }
890
- : disabledItem('+', 'Create user'));
855
+ : disabledItem('', `Databases (${databases.length})`));
891
856
  }
892
- // Backup - requires database selection for multi-db containers
893
- actionChoices.push(canDoDbAction
894
- ? { name: `${chalk.magenta('↓')} Backup database`, value: 'backup' }
895
- : disabledItem('↓', 'Backup database'));
896
- // Restore - requires database selection for multi-db containers
897
- actionChoices.push(canDoDbAction
898
- ? { name: `${chalk.magenta('↑')} Restore from backup`, value: 'restore' }
899
- : disabledItem('↑', 'Restore from backup'));
900
- // Export - server-based DBs must be running, file-based must have the file
857
+ else {
858
+ // Single-db: all data actions inline
859
+ // Open console
860
+ actionChoices.push(canDoDbAction
861
+ ? { name: `${chalk.blue('>')} Open console`, value: 'shell' }
862
+ : disabledItem('>', 'Open console'));
863
+ // Run script file
864
+ // Label comes from engines.json scriptFileLabel; null means no script support (REST API engines)
865
+ const engineConfig = await getEngineConfig(config.engine);
866
+ if (engineConfig.scriptFileLabel) {
867
+ const runScriptLabel = engineConfig.scriptFileLabel;
868
+ actionChoices.push(canDoDbAction
869
+ ? {
870
+ name: `${chalk.yellow('▷')} ${runScriptLabel}`,
871
+ value: 'run-sql',
872
+ }
873
+ : disabledItem('▷', runScriptLabel));
874
+ }
875
+ // Copy connection string
876
+ actionChoices.push(canDoDbAction
877
+ ? {
878
+ name: `${chalk.green('⎘')} Copy connection string`,
879
+ value: 'copy',
880
+ }
881
+ : disabledItem('⎘', 'Copy connection string'));
882
+ // Create user - only for engines that override createUser from BaseEngine
883
+ const engine = getEngine(config.engine);
884
+ const supportsUsers = engine.createUser !== BaseEngine.prototype.createUser;
885
+ if (supportsUsers) {
886
+ actionChoices.push(containerReady
887
+ ? {
888
+ name: `${chalk.yellow('+')} Create user`,
889
+ value: 'create_user',
890
+ }
891
+ : disabledItem('+', 'Create user'));
892
+ }
893
+ // Create database - only for engines that support it, requires running
894
+ if (canCreateDatabase(config.engine)) {
895
+ actionChoices.push(containerReady
896
+ ? {
897
+ name: `${chalk.green('+')} Create database`,
898
+ value: 'create_database',
899
+ }
900
+ : disabledItem('+', 'Create database'));
901
+ }
902
+ // Rename database - only when engine supports it AND container has databases
903
+ if (canRenameDatabase(config.engine) && databases.length > 0) {
904
+ actionChoices.push(containerReady
905
+ ? {
906
+ name: `${chalk.yellow('⇄')} Rename database`,
907
+ value: 'rename_database',
908
+ }
909
+ : disabledItem('⇄', 'Rename database'));
910
+ }
911
+ // Drop database - only when engine supports it AND has >1 database (can't drop default)
912
+ if (canDropDatabase(config.engine) && databases.length > 1) {
913
+ actionChoices.push(containerReady
914
+ ? {
915
+ name: `${chalk.red('−')} Drop database`,
916
+ value: 'drop_database',
917
+ }
918
+ : disabledItem('−', 'Drop database'));
919
+ }
920
+ // Backup
921
+ actionChoices.push(canDoDbAction
922
+ ? {
923
+ name: `${chalk.magenta('↓')} Backup database`,
924
+ value: 'backup',
925
+ }
926
+ : disabledItem('↓', 'Backup database'));
927
+ // Restore
928
+ actionChoices.push(canDoDbAction
929
+ ? {
930
+ name: `${chalk.magenta('↑')} Restore from backup`,
931
+ value: 'restore',
932
+ }
933
+ : disabledItem('↑', 'Restore from backup'));
934
+ }
935
+ // Export - always container-level (server-based must be running, file-based must have the file)
901
936
  actionChoices.push(containerReady
902
937
  ? { name: `${chalk.cyan('⬆')} Export`, value: 'export' }
903
938
  : disabledItem('⬆', 'Export'));
@@ -957,47 +992,31 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
957
992
  await showMainMenu();
958
993
  return;
959
994
  }
960
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
995
+ await showContainerSubmenu(containerName, showMainMenu);
961
996
  return;
962
997
  }
963
998
  case 'stop':
964
999
  await handleStopContainer(containerName);
965
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1000
+ await showContainerSubmenu(containerName, showMainMenu);
966
1001
  return;
967
- case 'select-database': {
968
- const result = await handleSelectDatabase(containerName, databases, config.database);
969
- if (result.action === 'home') {
970
- await showMainMenu();
971
- return;
972
- }
973
- if (result.action === 'back') {
974
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
975
- return;
976
- }
977
- if (result.action === 'change-default') {
978
- await handleChangeDefaultDatabase(containerName, databases, config.database);
979
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
980
- return;
981
- }
982
- // action === 'select'
983
- await showContainerSubmenu(containerName, showMainMenu, result.database);
1002
+ case 'databases':
1003
+ await showDatabasesSubmenu(containerName, showMainMenu);
984
1004
  return;
985
- }
986
1005
  case 'shell':
987
1006
  await handleOpenShell(containerName, activeDatabase);
988
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1007
+ await showContainerSubmenu(containerName, showMainMenu);
989
1008
  return;
990
1009
  case 'run-sql':
991
1010
  await handleRunSql(containerName, activeDatabase);
992
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1011
+ await showContainerSubmenu(containerName, showMainMenu);
993
1012
  return;
994
1013
  case 'logs':
995
1014
  await handleViewLogs(containerName);
996
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1015
+ await showContainerSubmenu(containerName, showMainMenu);
997
1016
  return;
998
1017
  case 'stop-pgweb':
999
1018
  await stopPgwebProcess(containerName, config.engine);
1000
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1019
+ await showContainerSubmenu(containerName, showMainMenu);
1001
1020
  return;
1002
1021
  case 'edit': {
1003
1022
  const newName = await handleEditContainer(containerName);
@@ -1007,10 +1026,10 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
1007
1026
  }
1008
1027
  if (newName !== containerName) {
1009
1028
  // Container was renamed, show submenu with new name
1010
- await showContainerSubmenu(newName, showMainMenu, activeDatabase);
1029
+ await showContainerSubmenu(newName, showMainMenu);
1011
1030
  }
1012
1031
  else {
1013
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1032
+ await showContainerSubmenu(containerName, showMainMenu);
1014
1033
  }
1015
1034
  return;
1016
1035
  }
@@ -1019,19 +1038,31 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
1019
1038
  return;
1020
1039
  case 'copy':
1021
1040
  await handleCopyConnectionString(containerName, activeDatabase);
1022
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1041
+ await showContainerSubmenu(containerName, showMainMenu);
1023
1042
  return;
1024
1043
  case 'create_user':
1025
1044
  await handleCreateUser(containerName, activeDatabase);
1026
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1045
+ await showContainerSubmenu(containerName, showMainMenu);
1046
+ return;
1047
+ case 'create_database':
1048
+ await handleCreateDatabase(containerName);
1049
+ await showContainerSubmenu(containerName, showMainMenu);
1050
+ return;
1051
+ case 'rename_database':
1052
+ await handleRenameDatabase(containerName);
1053
+ await showContainerSubmenu(containerName, showMainMenu);
1054
+ return;
1055
+ case 'drop_database':
1056
+ await handleDropDatabase(containerName);
1057
+ await showContainerSubmenu(containerName, showMainMenu);
1027
1058
  return;
1028
1059
  case 'backup':
1029
1060
  await handleBackupForContainer(containerName, activeDatabase);
1030
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1061
+ await showContainerSubmenu(containerName, showMainMenu);
1031
1062
  return;
1032
1063
  case 'restore':
1033
1064
  await handleRestoreForContainer(containerName, activeDatabase);
1034
- await showContainerSubmenu(containerName, showMainMenu, activeDatabase);
1065
+ await showContainerSubmenu(containerName, showMainMenu);
1035
1066
  return;
1036
1067
  case 'detach':
1037
1068
  await handleDetachContainer(containerName, showMainMenu);
@@ -1049,6 +1080,272 @@ export async function showContainerSubmenu(containerName, showMainMenu, selected
1049
1080
  return; // Return to main menu
1050
1081
  }
1051
1082
  }
1083
+ async function showDatabasesSubmenu(containerName, showMainMenu) {
1084
+ const config = await containerManager.getConfig(containerName);
1085
+ if (!config) {
1086
+ console.error(uiError(`Container "${containerName}" not found`));
1087
+ return;
1088
+ }
1089
+ const databases = config.databases || [config.database];
1090
+ // If only 1 database remains, return to container submenu (inline mode)
1091
+ if (databases.length <= 1) {
1092
+ await showContainerSubmenu(containerName, showMainMenu);
1093
+ return;
1094
+ }
1095
+ const engineIcon = getEngineIcon(config.engine);
1096
+ console.clear();
1097
+ console.log(header(`${engineIcon} ${containerName} - Databases`));
1098
+ console.log();
1099
+ const choices = databases.map((db) => ({
1100
+ name: db === config.database ? `${db} ${chalk.gray('(default)')}` : db,
1101
+ value: db,
1102
+ }));
1103
+ choices.push(new inquirer.Separator());
1104
+ // Create database - only if engine supports it AND container is running
1105
+ const isFileBased = isFileBasedEngine(config.engine);
1106
+ const containerReady = isFileBased
1107
+ ? existsSync(config.database)
1108
+ : await processManager.isRunning(containerName, { engine: config.engine });
1109
+ if (canCreateDatabase(config.engine) && containerReady) {
1110
+ choices.push({
1111
+ name: `${chalk.green('+')} Create database`,
1112
+ value: '_create_database',
1113
+ });
1114
+ }
1115
+ choices.push({
1116
+ name: `${chalk.blue('←')} Back`,
1117
+ value: '_back',
1118
+ });
1119
+ choices.push({
1120
+ name: `${chalk.blue('⌂')} Home ${chalk.gray('(esc)')}`,
1121
+ value: '_home',
1122
+ });
1123
+ choices.push(new inquirer.Separator());
1124
+ const { selection } = await escapeablePrompt([
1125
+ {
1126
+ type: 'list',
1127
+ name: 'selection',
1128
+ message: 'Select a database:',
1129
+ choices,
1130
+ pageSize: getPageSize(),
1131
+ },
1132
+ ]);
1133
+ switch (selection) {
1134
+ case '_create_database':
1135
+ await handleCreateDatabase(containerName);
1136
+ await showDatabasesSubmenu(containerName, showMainMenu);
1137
+ return;
1138
+ case '_back':
1139
+ await showContainerSubmenu(containerName, showMainMenu);
1140
+ return;
1141
+ case '_home':
1142
+ return;
1143
+ default:
1144
+ await showDatabaseActionMenu(containerName, selection, showMainMenu);
1145
+ return;
1146
+ }
1147
+ }
1148
+ async function showDatabaseActionMenu(containerName, database, showMainMenu) {
1149
+ const config = await containerManager.getConfig(containerName);
1150
+ if (!config) {
1151
+ console.error(uiError(`Container "${containerName}" not found`));
1152
+ return;
1153
+ }
1154
+ const databases = config.databases || [config.database];
1155
+ // If the database no longer exists, return to databases submenu
1156
+ if (!databases.includes(database)) {
1157
+ await showDatabasesSubmenu(containerName, showMainMenu);
1158
+ return;
1159
+ }
1160
+ const engineIcon = getEngineIcon(config.engine);
1161
+ const headerText = `${engineIcon} ${containerName} ${chalk.gray('→')} ${database}`;
1162
+ console.clear();
1163
+ console.log(header(headerText));
1164
+ const isFileBased = isFileBasedEngine(config.engine);
1165
+ const isRunning = isFileBased
1166
+ ? existsSync(config.database)
1167
+ : await processManager.isRunning(containerName, { engine: config.engine });
1168
+ const containerReady = isRunning;
1169
+ console.log(chalk.gray(`${config.engine} ${config.version} - ${isRunning ? 'running' : 'stopped'}`));
1170
+ console.log();
1171
+ const actionChoices = [];
1172
+ // Data operations section
1173
+ const dataSectionLabel = containerReady
1174
+ ? isFileBased
1175
+ ? 'Available'
1176
+ : 'Running'
1177
+ : isFileBased
1178
+ ? 'Database file missing'
1179
+ : 'Start container first';
1180
+ actionChoices.push(new inquirer.Separator(chalk.gray(`── ${dataSectionLabel} ──`)));
1181
+ // Open console
1182
+ actionChoices.push(containerReady
1183
+ ? { name: `${chalk.blue('>')} Open console`, value: 'shell' }
1184
+ : disabledItem('>', 'Open console'));
1185
+ // Run script file
1186
+ const engineConfig = await getEngineConfig(config.engine);
1187
+ if (engineConfig.scriptFileLabel) {
1188
+ actionChoices.push(containerReady
1189
+ ? {
1190
+ name: `${chalk.yellow('▷')} ${engineConfig.scriptFileLabel}`,
1191
+ value: 'run-sql',
1192
+ }
1193
+ : disabledItem('▷', engineConfig.scriptFileLabel));
1194
+ }
1195
+ // Copy connection string
1196
+ actionChoices.push(containerReady
1197
+ ? {
1198
+ name: `${chalk.green('⎘')} Copy connection string`,
1199
+ value: 'copy',
1200
+ }
1201
+ : disabledItem('⎘', 'Copy connection string'));
1202
+ // Create user
1203
+ const engine = getEngine(config.engine);
1204
+ const supportsUsers = engine.createUser !== BaseEngine.prototype.createUser;
1205
+ if (supportsUsers) {
1206
+ actionChoices.push(containerReady
1207
+ ? { name: `${chalk.yellow('+')} Create user`, value: 'create_user' }
1208
+ : disabledItem('+', 'Create user'));
1209
+ }
1210
+ // Rename database
1211
+ if (canRenameDatabase(config.engine)) {
1212
+ actionChoices.push(containerReady
1213
+ ? {
1214
+ name: `${chalk.yellow('⇄')} Rename database`,
1215
+ value: 'rename_database',
1216
+ }
1217
+ : disabledItem('⇄', 'Rename database'));
1218
+ }
1219
+ // Drop database - can't drop the default database
1220
+ if (canDropDatabase(config.engine) && database !== config.database) {
1221
+ actionChoices.push(containerReady
1222
+ ? {
1223
+ name: `${chalk.red('−')} Drop database`,
1224
+ value: 'drop_database',
1225
+ }
1226
+ : disabledItem('−', 'Drop database'));
1227
+ }
1228
+ // Backup
1229
+ actionChoices.push(containerReady
1230
+ ? { name: `${chalk.magenta('↓')} Backup database`, value: 'backup' }
1231
+ : disabledItem('↓', 'Backup database'));
1232
+ // Restore
1233
+ actionChoices.push(containerReady
1234
+ ? {
1235
+ name: `${chalk.magenta('↑')} Restore from backup`,
1236
+ value: 'restore',
1237
+ }
1238
+ : disabledItem('↑', 'Restore from backup'));
1239
+ actionChoices.push(new inquirer.Separator());
1240
+ // Set as default
1241
+ if (database !== config.database) {
1242
+ actionChoices.push({
1243
+ name: `${chalk.yellow('★')} Set as default`,
1244
+ value: 'set_default',
1245
+ });
1246
+ }
1247
+ // Navigation
1248
+ actionChoices.push({
1249
+ name: `${chalk.blue('←')} Back to databases`,
1250
+ value: 'back',
1251
+ });
1252
+ actionChoices.push({
1253
+ name: `${chalk.blue('⌂')} Home ${chalk.gray('(esc)')}`,
1254
+ value: 'home',
1255
+ });
1256
+ actionChoices.push(new inquirer.Separator());
1257
+ const { action } = await escapeablePrompt([
1258
+ {
1259
+ type: 'list',
1260
+ name: 'action',
1261
+ message: 'What would you like to do?',
1262
+ choices: actionChoices,
1263
+ pageSize: getPageSize(),
1264
+ },
1265
+ ]);
1266
+ switch (action) {
1267
+ case 'shell':
1268
+ await handleOpenShell(containerName, database);
1269
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1270
+ return;
1271
+ case 'run-sql':
1272
+ await handleRunSql(containerName, database);
1273
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1274
+ return;
1275
+ case 'copy':
1276
+ await handleCopyConnectionString(containerName, database);
1277
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1278
+ return;
1279
+ case 'create_user':
1280
+ await handleCreateUser(containerName, database);
1281
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1282
+ return;
1283
+ case 'rename_database': {
1284
+ // After rename, the database name may have changed
1285
+ const beforeDbs = [...databases];
1286
+ await handleRenameDatabase(containerName, database);
1287
+ const afterConfig = await containerManager.getConfig(containerName);
1288
+ const afterDbs = afterConfig?.databases ||
1289
+ [afterConfig?.database].filter(Boolean);
1290
+ // Find the new database name (appeared after rename but not before)
1291
+ const newDb = afterDbs.find((db) => !beforeDbs.includes(db));
1292
+ if (newDb) {
1293
+ // Database was renamed — show the new database's action menu
1294
+ await showDatabaseActionMenu(containerName, newDb, showMainMenu);
1295
+ }
1296
+ else {
1297
+ // Rename failed or user kept original — stay on current database
1298
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1299
+ }
1300
+ return;
1301
+ }
1302
+ case 'drop_database': {
1303
+ await handleDropDatabase(containerName, database);
1304
+ // Check if the database still exists after drop
1305
+ const freshConfig = await containerManager.getConfig(containerName);
1306
+ if (!freshConfig) {
1307
+ // Container was deleted — return to main menu
1308
+ return;
1309
+ }
1310
+ const freshDbs = freshConfig.databases || [freshConfig.database];
1311
+ if (!freshDbs.includes(database)) {
1312
+ // Database was dropped
1313
+ if ((freshDbs?.length ?? 0) <= 1) {
1314
+ // Only 1 database left — no longer multi-db, go to container submenu
1315
+ await showContainerSubmenu(containerName, showMainMenu);
1316
+ }
1317
+ else {
1318
+ await showDatabasesSubmenu(containerName, showMainMenu);
1319
+ }
1320
+ }
1321
+ else {
1322
+ // Database still exists (user cancelled) — stay in per-db menu
1323
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1324
+ }
1325
+ return;
1326
+ }
1327
+ case 'backup':
1328
+ await handleBackupForContainer(containerName, database);
1329
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1330
+ return;
1331
+ case 'restore':
1332
+ await handleRestoreForContainer(containerName, database);
1333
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1334
+ return;
1335
+ case 'set_default':
1336
+ await containerManager.updateConfig(containerName, { database });
1337
+ console.log();
1338
+ console.log(uiSuccess(`Default database changed to "${database}"`));
1339
+ await pressEnterToContinue();
1340
+ await showDatabaseActionMenu(containerName, database, showMainMenu);
1341
+ return;
1342
+ case 'back':
1343
+ await showDatabasesSubmenu(containerName, showMainMenu);
1344
+ return;
1345
+ case 'home':
1346
+ return;
1347
+ }
1348
+ }
1052
1349
  export async function handleStart() {
1053
1350
  const containers = await containerManager.list();
1054
1351
  // Filter for stopped containers, excluding file-based DBs and linked containers
@@ -1454,78 +1751,6 @@ async function handleEditContainer(containerName) {
1454
1751
  }
1455
1752
  return containerName;
1456
1753
  }
1457
- async function handleSelectDatabase(containerName, databases, primaryDatabase) {
1458
- console.clear();
1459
- console.log(header(`${containerName} - Select Database`));
1460
- console.log();
1461
- const choices = databases.map((db) => ({
1462
- name: db === primaryDatabase ? `${db} ${chalk.gray('(default)')}` : db,
1463
- value: db,
1464
- }));
1465
- choices.push(new inquirer.Separator());
1466
- choices.push({
1467
- name: `${chalk.yellow('★')} Change default database`,
1468
- value: '_change-default',
1469
- });
1470
- choices.push({
1471
- name: `${chalk.blue('←')} Back`,
1472
- value: '_back',
1473
- });
1474
- choices.push({
1475
- name: `${chalk.blue('⌂')} Home ${chalk.gray('(esc)')}`,
1476
- value: '_home',
1477
- });
1478
- const { database } = await escapeablePrompt([
1479
- {
1480
- type: 'list',
1481
- name: 'database',
1482
- message: 'Select a database:',
1483
- choices,
1484
- pageSize: getPageSize(),
1485
- },
1486
- ]);
1487
- if (database === '_back') {
1488
- return { action: 'back' };
1489
- }
1490
- if (database === '_home') {
1491
- return { action: 'home' };
1492
- }
1493
- if (database === '_change-default') {
1494
- return { action: 'change-default' };
1495
- }
1496
- return { action: 'select', database };
1497
- }
1498
- async function handleChangeDefaultDatabase(containerName, databases, currentDefault) {
1499
- console.clear();
1500
- console.log(header(`${containerName} - Change Default Database`));
1501
- console.log();
1502
- const choices = databases.map((db) => ({
1503
- name: db === currentDefault ? `${db} ${chalk.gray('(current default)')}` : db,
1504
- value: db,
1505
- }));
1506
- choices.push(new inquirer.Separator());
1507
- choices.push({
1508
- name: `${chalk.blue('←')} Cancel`,
1509
- value: '_cancel',
1510
- });
1511
- const { database } = await escapeablePrompt([
1512
- {
1513
- type: 'list',
1514
- name: 'database',
1515
- message: 'Select new default database:',
1516
- choices,
1517
- pageSize: getPageSize(),
1518
- },
1519
- ]);
1520
- if (database === '_cancel' || database === currentDefault) {
1521
- return;
1522
- }
1523
- // Update the container config
1524
- await containerManager.updateConfig(containerName, { database });
1525
- console.log();
1526
- console.log(uiSuccess(`Default database changed to "${database}"`));
1527
- await pressEnterToContinue();
1528
- }
1529
1754
  async function handleCloneFromSubmenu(sourceName, showMainMenu) {
1530
1755
  const sourceConfig = await containerManager.getConfig(sourceName);
1531
1756
  if (!sourceConfig) {
@@ -1712,7 +1937,7 @@ async function handleExportSubmenu(containerName, databases, showMainMenu) {
1712
1937
  await handleExportDocker(containerName, databases, showMainMenu);
1713
1938
  return;
1714
1939
  case 'back':
1715
- await showContainerSubmenu(containerName, showMainMenu, undefined);
1940
+ await showContainerSubmenu(containerName, showMainMenu);
1716
1941
  return;
1717
1942
  case 'home':
1718
1943
  await showMainMenu();
@@ -1747,7 +1972,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1747
1972
  if (!config) {
1748
1973
  console.log(uiError(`Container "${containerName}" not found`));
1749
1974
  await pressEnterToContinue();
1750
- await showContainerSubmenu(containerName, showMainMenu, undefined);
1975
+ await showContainerSubmenu(containerName, showMainMenu);
1751
1976
  return;
1752
1977
  }
1753
1978
  const engine = getEngine(config.engine);
@@ -1762,7 +1987,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1762
1987
  if (!shouldOverwrite) {
1763
1988
  console.log(uiInfo('Export cancelled'));
1764
1989
  await pressEnterToContinue();
1765
- await showContainerSubmenu(containerName, showMainMenu, undefined);
1990
+ await showContainerSubmenu(containerName, showMainMenu);
1766
1991
  return;
1767
1992
  }
1768
1993
  // Remove existing directory
@@ -1772,7 +1997,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1772
1997
  catch (error) {
1773
1998
  console.log(uiError(`Failed to remove existing directory: ${error.message}`));
1774
1999
  await pressEnterToContinue();
1775
- await showContainerSubmenu(containerName, showMainMenu, undefined);
2000
+ await showContainerSubmenu(containerName, showMainMenu);
1776
2001
  return;
1777
2002
  }
1778
2003
  }
@@ -1829,7 +2054,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1829
2054
  copySpinner.fail('Failed to copy database file');
1830
2055
  console.log(uiError(error.message));
1831
2056
  await pressEnterToContinue();
1832
- await showContainerSubmenu(containerName, showMainMenu, undefined);
2057
+ await showContainerSubmenu(containerName, showMainMenu);
1833
2058
  return;
1834
2059
  }
1835
2060
  }
@@ -1859,7 +2084,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1859
2084
  backupSpinner.fail('Backup failed');
1860
2085
  console.log(uiError(error.message));
1861
2086
  await pressEnterToContinue();
1862
- await showContainerSubmenu(containerName, showMainMenu, undefined);
2087
+ await showContainerSubmenu(containerName, showMainMenu);
1863
2088
  return;
1864
2089
  }
1865
2090
  }
@@ -1908,7 +2133,7 @@ async function handleExportDocker(containerName, databases, showMainMenu) {
1908
2133
  console.log(uiError(error.message));
1909
2134
  }
1910
2135
  await pressEnterToContinue();
1911
- await showContainerSubmenu(containerName, showMainMenu, undefined);
2136
+ await showContainerSubmenu(containerName, showMainMenu);
1912
2137
  }
1913
2138
  async function handleCreateUser(containerName, activeDatabase) {
1914
2139
  const config = await containerManager.getConfig(containerName);
@@ -2013,4 +2238,392 @@ async function handleCreateUser(containerName, activeDatabase) {
2013
2238
  }
2014
2239
  await pressEnterToContinue();
2015
2240
  }
2241
+ async function handleCreateDatabase(containerName) {
2242
+ const config = await containerManager.getConfig(containerName);
2243
+ if (!config) {
2244
+ console.log(uiError(`Container "${containerName}" not found`));
2245
+ await pressEnterToContinue();
2246
+ return;
2247
+ }
2248
+ const isRunning = await processManager.isRunning(containerName, {
2249
+ engine: config.engine,
2250
+ });
2251
+ if (!isRunning) {
2252
+ console.log(uiError(`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`));
2253
+ await pressEnterToContinue();
2254
+ return;
2255
+ }
2256
+ // Prompt for name — Escape returns to submenu
2257
+ let dbName;
2258
+ try {
2259
+ const result = await escapeablePrompt([
2260
+ {
2261
+ type: 'input',
2262
+ name: 'dbName',
2263
+ message: 'New database name:',
2264
+ validate: (input) => {
2265
+ if (!input.trim())
2266
+ return 'Database name is required';
2267
+ if (/\s/.test(input))
2268
+ return 'Database name cannot contain spaces';
2269
+ return true;
2270
+ },
2271
+ },
2272
+ ]);
2273
+ dbName = result.dbName;
2274
+ }
2275
+ catch (error) {
2276
+ if (error instanceof EscapeError)
2277
+ return;
2278
+ throw error;
2279
+ }
2280
+ // Check if already exists
2281
+ const rawDatabases = config.databases || [];
2282
+ const trackedDatabases = [...new Set([config.database, ...rawDatabases])];
2283
+ if (trackedDatabases.includes(dbName)) {
2284
+ console.log(uiError(`Database "${dbName}" already exists in "${containerName}"`));
2285
+ await pressEnterToContinue();
2286
+ return;
2287
+ }
2288
+ const engine = getEngine(config.engine);
2289
+ // Check if database exists on the server (not just tracking)
2290
+ try {
2291
+ const serverDatabases = await engine.listDatabases(config);
2292
+ if (serverDatabases.includes(dbName)) {
2293
+ console.log(uiError(`Database "${dbName}" already exists on the server. Use "spindb databases add ${containerName} ${dbName}" to track it.`));
2294
+ await pressEnterToContinue();
2295
+ return;
2296
+ }
2297
+ }
2298
+ catch {
2299
+ // listDatabases not supported for all engines — proceed
2300
+ }
2301
+ try {
2302
+ const spinner = createSpinner(`Creating database "${dbName}" in "${containerName}"...`);
2303
+ spinner.start();
2304
+ try {
2305
+ await engine.createDatabase(config, dbName);
2306
+ spinner.succeed(`Created database "${dbName}"`);
2307
+ }
2308
+ catch (error) {
2309
+ spinner.fail(`Failed to create database "${dbName}"`);
2310
+ throw error;
2311
+ }
2312
+ await containerManager.addDatabase(containerName, dbName);
2313
+ const connectionString = engine.getConnectionString(config, dbName);
2314
+ console.log();
2315
+ console.log(uiSuccess(`Database "${dbName}" created in "${containerName}"`));
2316
+ console.log();
2317
+ console.log(chalk.gray(' Connection:'), chalk.cyan(connectionString));
2318
+ }
2319
+ catch (error) {
2320
+ console.log(uiError(error.message));
2321
+ }
2322
+ await pressEnterToContinue();
2323
+ }
2324
+ async function handleRenameDatabase(containerName, targetDatabase) {
2325
+ const config = await containerManager.getConfig(containerName);
2326
+ if (!config) {
2327
+ console.log(uiError(`Container "${containerName}" not found`));
2328
+ await pressEnterToContinue();
2329
+ return;
2330
+ }
2331
+ const isRunning = await processManager.isRunning(containerName, {
2332
+ engine: config.engine,
2333
+ });
2334
+ if (!isRunning) {
2335
+ console.log(uiError(`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`));
2336
+ await pressEnterToContinue();
2337
+ return;
2338
+ }
2339
+ const rawDatabases = config.databases || [];
2340
+ const trackedDatabases = [...new Set([config.database, ...rawDatabases])];
2341
+ if (trackedDatabases.length === 0) {
2342
+ console.log(uiError(`No databases to rename in "${containerName}"`));
2343
+ await pressEnterToContinue();
2344
+ return;
2345
+ }
2346
+ // Select database to rename — skip prompt if targetDatabase provided
2347
+ let oldName;
2348
+ if (targetDatabase) {
2349
+ oldName = targetDatabase;
2350
+ }
2351
+ else {
2352
+ try {
2353
+ const result = await escapeablePrompt([
2354
+ {
2355
+ type: 'list',
2356
+ name: 'oldName',
2357
+ message: 'Select database to rename:',
2358
+ choices: trackedDatabases.map((db) => {
2359
+ const isDefault = db === config.database;
2360
+ return {
2361
+ name: isDefault ? `${db} (default)` : db,
2362
+ value: db,
2363
+ };
2364
+ }),
2365
+ },
2366
+ ]);
2367
+ oldName = result.oldName;
2368
+ }
2369
+ catch (error) {
2370
+ if (error instanceof EscapeError)
2371
+ return;
2372
+ throw error;
2373
+ }
2374
+ }
2375
+ // Enter new name — Escape returns to submenu
2376
+ let newName;
2377
+ try {
2378
+ const result = await escapeablePrompt([
2379
+ {
2380
+ type: 'input',
2381
+ name: 'newName',
2382
+ message: `New name for "${oldName}":`,
2383
+ validate: (input) => {
2384
+ if (!input.trim())
2385
+ return 'Database name is required';
2386
+ if (/\s/.test(input))
2387
+ return 'Database name cannot contain spaces';
2388
+ if (input === oldName)
2389
+ return 'New name must be different';
2390
+ if (trackedDatabases.includes(input))
2391
+ return `"${input}" already exists`;
2392
+ return true;
2393
+ },
2394
+ },
2395
+ ]);
2396
+ newName = result.newName;
2397
+ }
2398
+ catch (error) {
2399
+ if (error instanceof EscapeError)
2400
+ return;
2401
+ throw error;
2402
+ }
2403
+ try {
2404
+ const isPrimaryRename = oldName === config.database;
2405
+ if (isPrimaryRename) {
2406
+ console.log(uiWarning(`This will rename the default database. The default will be updated to "${newName}".`));
2407
+ }
2408
+ const caps = getDatabaseCapabilities(config.engine);
2409
+ const engine = getEngine(config.engine);
2410
+ let shouldDrop = true;
2411
+ let dropSucceeded = false;
2412
+ let backupPath;
2413
+ if (caps.supportsRename === 'native') {
2414
+ // Native rename (PostgreSQL, ClickHouse, CockroachDB, Meilisearch) — instant, no backup needed
2415
+ const spinner = createSpinner(`Renaming "${oldName}" to "${newName}"...`);
2416
+ spinner.start();
2417
+ try {
2418
+ await engine.renameDatabase(config, oldName, newName);
2419
+ spinner.succeed(`Renamed "${oldName}" to "${newName}"`);
2420
+ dropSucceeded = true; // Native rename atomically replaces the old name
2421
+ }
2422
+ catch (error) {
2423
+ spinner.fail(`Failed to rename database`);
2424
+ throw error;
2425
+ }
2426
+ }
2427
+ else {
2428
+ // Backup/restore rename — explain strategy to user
2429
+ console.log();
2430
+ console.log(uiInfo(`${engine.displayName} does not support native database renaming.`));
2431
+ console.log(chalk.gray(` We'll clone "${oldName}" to a new database named "${newName}",`));
2432
+ console.log(chalk.gray(` then ask if you'd like to delete the original.`));
2433
+ console.log();
2434
+ await mkdir(paths.renameBackups, { recursive: true });
2435
+ const format = getDefaultFormat(config.engine);
2436
+ const extension = getBackupExtension(config.engine, format);
2437
+ const timestamp = new Date()
2438
+ .toISOString()
2439
+ .replace(/[:.]/g, '')
2440
+ .slice(0, 15);
2441
+ const backupFileName = `${containerName}-${oldName}-rename-${timestamp}${extension}`;
2442
+ backupPath = join(paths.renameBackups, backupFileName);
2443
+ const spinner = createSpinner(`Backing up "${oldName}"...`);
2444
+ spinner.start();
2445
+ // Step 1: Backup
2446
+ try {
2447
+ await engine.backup(config, backupPath, {
2448
+ database: oldName,
2449
+ format,
2450
+ });
2451
+ }
2452
+ catch (error) {
2453
+ spinner.fail(`Failed to backup "${oldName}"`);
2454
+ throw error;
2455
+ }
2456
+ // Step 2: Create new database
2457
+ spinner.text = `Creating database "${newName}"...`;
2458
+ try {
2459
+ await engine.createDatabase(config, newName);
2460
+ }
2461
+ catch (error) {
2462
+ spinner.fail(`Failed to create database "${newName}"`);
2463
+ throw error;
2464
+ }
2465
+ // Step 3: Restore data
2466
+ spinner.text = `Restoring data to "${newName}"...`;
2467
+ try {
2468
+ await engine.restore({ ...config, database: newName }, backupPath, {
2469
+ database: newName,
2470
+ });
2471
+ }
2472
+ catch (error) {
2473
+ spinner.fail(`Failed to restore data to "${newName}"`);
2474
+ // Rollback: drop new DB, keep backup
2475
+ try {
2476
+ await engine.dropDatabase(config, newName);
2477
+ }
2478
+ catch {
2479
+ console.log(uiWarning(`Could not remove partially-created database "${newName}" — manual cleanup may be needed`));
2480
+ }
2481
+ console.log(uiWarning(`Safety backup retained at: ${backupPath}`));
2482
+ throw error;
2483
+ }
2484
+ spinner.succeed(`Cloned "${oldName}" to "${newName}"`);
2485
+ // Ask whether to delete the original
2486
+ console.log();
2487
+ console.log(uiSuccess('Database clone successful.'));
2488
+ try {
2489
+ shouldDrop = await promptConfirm(`Delete the original database "${oldName}"?`, true);
2490
+ }
2491
+ catch (error) {
2492
+ if (error instanceof EscapeError) {
2493
+ shouldDrop = false; // Escape = keep the original
2494
+ }
2495
+ else {
2496
+ throw error;
2497
+ }
2498
+ }
2499
+ if (shouldDrop) {
2500
+ const dropSpinner = createSpinner(`Dropping old database "${oldName}"...`);
2501
+ dropSpinner.start();
2502
+ try {
2503
+ await engine.terminateConnections(config, oldName);
2504
+ await engine.dropDatabase(config, oldName);
2505
+ dropSpinner.succeed(`Dropped old database "${oldName}"`);
2506
+ dropSucceeded = true;
2507
+ }
2508
+ catch (error) {
2509
+ dropSpinner.warn(`Could not drop "${oldName}": ${error.message}`);
2510
+ // Non-fatal — data is safely in new database
2511
+ }
2512
+ }
2513
+ else {
2514
+ console.log(chalk.gray(` Kept "${oldName}" — both databases now exist.`));
2515
+ }
2516
+ }
2517
+ // Update tracking
2518
+ await updateRenameTracking(containerName, oldName, newName, {
2519
+ shouldDrop: dropSucceeded,
2520
+ isPrimaryRename,
2521
+ });
2522
+ // Summary
2523
+ const connectionString = engine.getConnectionString({ ...config, database: newName }, newName);
2524
+ console.log();
2525
+ console.log(uiSuccess(`Renamed "${oldName}" → "${newName}"`));
2526
+ console.log();
2527
+ console.log(chalk.gray(' Connection:'), chalk.cyan(connectionString));
2528
+ if (backupPath) {
2529
+ console.log(chalk.gray(' Backup: '), chalk.white(backupPath));
2530
+ }
2531
+ if (isPrimaryRename) {
2532
+ console.log(chalk.gray(` Default database updated to "${newName}".`));
2533
+ }
2534
+ }
2535
+ catch (error) {
2536
+ console.log(uiError(error.message));
2537
+ }
2538
+ await pressEnterToContinue();
2539
+ }
2540
+ async function handleDropDatabase(containerName, targetDatabase) {
2541
+ const config = await containerManager.getConfig(containerName);
2542
+ if (!config) {
2543
+ console.log(uiError(`Container "${containerName}" not found`));
2544
+ await pressEnterToContinue();
2545
+ return;
2546
+ }
2547
+ const isRunning = await processManager.isRunning(containerName, {
2548
+ engine: config.engine,
2549
+ });
2550
+ if (!isRunning) {
2551
+ console.log(uiError(`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`));
2552
+ await pressEnterToContinue();
2553
+ return;
2554
+ }
2555
+ // Select database to drop — skip prompt if targetDatabase provided
2556
+ let dbName;
2557
+ if (targetDatabase) {
2558
+ if (targetDatabase === config.database) {
2559
+ console.log(uiError(`Cannot drop the default database. Use "spindb delete" to remove the container.`));
2560
+ await pressEnterToContinue();
2561
+ return;
2562
+ }
2563
+ dbName = targetDatabase;
2564
+ }
2565
+ else {
2566
+ const rawDatabases = config.databases || [];
2567
+ const trackedDatabases = [...new Set([config.database, ...rawDatabases])];
2568
+ const droppable = trackedDatabases.filter((db) => db !== config.database);
2569
+ if (droppable.length === 0) {
2570
+ console.log(uiError(`No databases to drop. The default database cannot be dropped.`));
2571
+ await pressEnterToContinue();
2572
+ return;
2573
+ }
2574
+ try {
2575
+ console.log(chalk.gray(` Default database "${config.database}" is excluded — use "spindb delete" to remove the container.`));
2576
+ const result = await escapeablePrompt([
2577
+ {
2578
+ type: 'list',
2579
+ name: 'dbName',
2580
+ message: 'Select database to drop:',
2581
+ choices: droppable.map((db) => ({ name: db, value: db })),
2582
+ },
2583
+ ]);
2584
+ dbName = result.dbName;
2585
+ }
2586
+ catch (error) {
2587
+ if (error instanceof EscapeError)
2588
+ return;
2589
+ throw error;
2590
+ }
2591
+ }
2592
+ // Confirm — Escape cancels
2593
+ let confirm;
2594
+ try {
2595
+ confirm = await promptConfirm(`Drop database "${dbName}" from ${config.engine} container "${containerName}"? This cannot be undone.`, false);
2596
+ }
2597
+ catch (error) {
2598
+ if (error instanceof EscapeError)
2599
+ return;
2600
+ throw error;
2601
+ }
2602
+ if (!confirm) {
2603
+ console.log(chalk.gray('Cancelled.'));
2604
+ await pressEnterToContinue();
2605
+ return;
2606
+ }
2607
+ try {
2608
+ const engine = getEngine(config.engine);
2609
+ const spinner = createSpinner(`Dropping database "${dbName}" from "${containerName}"...`);
2610
+ spinner.start();
2611
+ try {
2612
+ await engine.terminateConnections(config, dbName);
2613
+ await engine.dropDatabase(config, dbName);
2614
+ spinner.succeed(`Dropped database "${dbName}"`);
2615
+ }
2616
+ catch (error) {
2617
+ spinner.fail(`Failed to drop database "${dbName}"`);
2618
+ throw error;
2619
+ }
2620
+ await containerManager.removeDatabase(containerName, dbName);
2621
+ console.log();
2622
+ console.log(uiSuccess(`Database "${dbName}" dropped from "${containerName}"`));
2623
+ }
2624
+ catch (error) {
2625
+ console.log(uiError(error.message));
2626
+ }
2627
+ await pressEnterToContinue();
2628
+ }
2016
2629
  //# sourceMappingURL=container-handlers.js.map