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.
- package/README.md +6 -1
- package/dist/cli/commands/databases.js +647 -12
- package/dist/cli/commands/databases.js.map +1 -1
- package/dist/cli/commands/menu/container-handlers.js +788 -175
- package/dist/cli/commands/menu/container-handlers.js.map +1 -1
- package/dist/cli/commands/query.js +3 -2
- package/dist/cli/commands/query.js.map +1 -1
- package/dist/config/paths.js +2 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/version.js +1 -1
- package/dist/core/container-manager.js +21 -0
- package/dist/core/container-manager.js.map +1 -1
- package/dist/core/database-capabilities.js +156 -0
- package/dist/core/database-capabilities.js.map +1 -0
- package/dist/engines/base-engine.js +8 -0
- package/dist/engines/base-engine.js.map +1 -1
- package/dist/engines/clickhouse/index.js +45 -0
- package/dist/engines/clickhouse/index.js.map +1 -1
- package/dist/engines/cockroachdb/index.js +45 -0
- package/dist/engines/cockroachdb/index.js.map +1 -1
- package/dist/engines/ferretdb/index.js +4 -3
- package/dist/engines/ferretdb/index.js.map +1 -1
- package/dist/engines/mariadb/index.js +12 -13
- package/dist/engines/mariadb/index.js.map +1 -1
- package/dist/engines/meilisearch/index.js +50 -1
- package/dist/engines/meilisearch/index.js.map +1 -1
- package/dist/engines/mongodb/index.js +46 -16
- package/dist/engines/mongodb/index.js.map +1 -1
- package/dist/engines/mysql/index.js +12 -13
- package/dist/engines/mysql/index.js.map +1 -1
- package/dist/engines/postgresql/index.js +54 -5
- package/dist/engines/postgresql/index.js.map +1 -1
- package/dist/engines/qdrant/index.js +2 -2
- package/dist/engines/qdrant/index.js.map +1 -1
- package/dist/engines/redis/index.js +15 -3
- package/dist/engines/redis/index.js.map +1 -1
- package/dist/engines/valkey/index.js +16 -4
- package/dist/engines/valkey/index.js.map +1 -1
- package/dist/engines/weaviate/index.js +1 -1
- package/dist/engines/weaviate/index.js.map +1 -1
- 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
|
|
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
|
-
//
|
|
712
|
-
const activeDatabase =
|
|
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
|
|
771
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
771
772
|
return;
|
|
772
773
|
case 'copy':
|
|
773
774
|
await handleCopyConnectionString(containerName, activeDatabase);
|
|
774
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
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
|
-
|
|
865
|
-
|
|
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.
|
|
888
|
-
value: '
|
|
852
|
+
name: `${chalk.cyan('◉')} Databases (${databases.length})`,
|
|
853
|
+
value: 'databases',
|
|
889
854
|
}
|
|
890
|
-
: disabledItem('
|
|
855
|
+
: disabledItem('◉', `Databases (${databases.length})`));
|
|
891
856
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
995
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
961
996
|
return;
|
|
962
997
|
}
|
|
963
998
|
case 'stop':
|
|
964
999
|
await handleStopContainer(containerName);
|
|
965
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
1000
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
966
1001
|
return;
|
|
967
|
-
case '
|
|
968
|
-
|
|
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
|
|
1007
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
989
1008
|
return;
|
|
990
1009
|
case 'run-sql':
|
|
991
1010
|
await handleRunSql(containerName, activeDatabase);
|
|
992
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
1011
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
993
1012
|
return;
|
|
994
1013
|
case 'logs':
|
|
995
1014
|
await handleViewLogs(containerName);
|
|
996
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
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
|
|
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
|
|
1029
|
+
await showContainerSubmenu(newName, showMainMenu);
|
|
1011
1030
|
}
|
|
1012
1031
|
else {
|
|
1013
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
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
|
|
1041
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
1023
1042
|
return;
|
|
1024
1043
|
case 'create_user':
|
|
1025
1044
|
await handleCreateUser(containerName, activeDatabase);
|
|
1026
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
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
|
|
1061
|
+
await showContainerSubmenu(containerName, showMainMenu);
|
|
1031
1062
|
return;
|
|
1032
1063
|
case 'restore':
|
|
1033
1064
|
await handleRestoreForContainer(containerName, activeDatabase);
|
|
1034
|
-
await showContainerSubmenu(containerName, showMainMenu
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|