spindb 0.31.4 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +107 -826
- package/cli/commands/create.ts +5 -1
- package/cli/commands/engines.ts +256 -1
- package/cli/commands/menu/backup-handlers.ts +16 -0
- package/cli/commands/menu/container-handlers.ts +170 -17
- package/cli/commands/menu/engine-handlers.ts +6 -0
- package/cli/commands/menu/settings-handlers.ts +6 -0
- package/cli/commands/menu/shell-handlers.ts +74 -14
- package/cli/commands/menu/sql-handlers.ts +8 -50
- package/cli/commands/menu/validators.ts +8 -0
- package/cli/commands/users.ts +264 -0
- package/cli/constants.ts +8 -0
- package/cli/helpers.ts +140 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +24 -20
- package/config/backup-formats.ts +28 -0
- package/config/engine-defaults.ts +26 -0
- package/config/engines-registry.ts +1 -0
- package/config/engines.json +50 -0
- package/config/engines.schema.json +6 -1
- package/core/base-binary-manager.ts +6 -1
- package/core/config-manager.ts +20 -0
- package/core/credential-manager.ts +257 -0
- package/core/dependency-manager.ts +5 -0
- package/core/docker-exporter.ts +30 -0
- package/core/error-handler.ts +19 -0
- package/engines/base-engine.ts +32 -1
- package/engines/clickhouse/index.ts +99 -3
- package/engines/cockroachdb/index.ts +69 -2
- package/engines/couchdb/index.ts +149 -1
- package/engines/ferretdb/README.md +4 -0
- package/engines/ferretdb/index.ts +342 -13
- package/engines/index.ts +8 -0
- package/engines/influxdb/README.md +180 -0
- package/engines/influxdb/api-client.ts +64 -0
- package/engines/influxdb/backup.ts +160 -0
- package/engines/influxdb/binary-manager.ts +110 -0
- package/engines/influxdb/binary-urls.ts +69 -0
- package/engines/influxdb/hostdb-releases.ts +23 -0
- package/engines/influxdb/index.ts +1227 -0
- package/engines/influxdb/restore.ts +417 -0
- package/engines/influxdb/version-maps.ts +75 -0
- package/engines/influxdb/version-validator.ts +128 -0
- package/engines/mariadb/index.ts +96 -1
- package/engines/meilisearch/index.ts +97 -1
- package/engines/mongodb/index.ts +82 -0
- package/engines/mysql/index.ts +105 -1
- package/engines/postgresql/index.ts +92 -0
- package/engines/qdrant/index.ts +107 -2
- package/engines/redis/index.ts +106 -12
- package/engines/surrealdb/index.ts +102 -2
- package/engines/typedb/backup.ts +167 -0
- package/engines/typedb/binary-manager.ts +200 -0
- package/engines/typedb/binary-urls.ts +38 -0
- package/engines/typedb/cli-utils.ts +210 -0
- package/engines/typedb/hostdb-releases.ts +118 -0
- package/engines/typedb/index.ts +1275 -0
- package/engines/typedb/restore.ts +377 -0
- package/engines/typedb/version-maps.ts +48 -0
- package/engines/typedb/version-validator.ts +127 -0
- package/engines/valkey/index.ts +70 -2
- package/package.json +4 -1
- package/types/index.ts +37 -0
package/cli/commands/create.ts
CHANGED
|
@@ -398,6 +398,10 @@ export function detectLocationType(location: string): {
|
|
|
398
398
|
return { type: 'connection', inferredEngine: Engine.Meilisearch }
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
if (location.startsWith('influxdb://')) {
|
|
402
|
+
return { type: 'connection', inferredEngine: Engine.InfluxDB }
|
|
403
|
+
}
|
|
404
|
+
|
|
401
405
|
if (existsSync(location)) {
|
|
402
406
|
// Check if it's a SQLite file (case-insensitive)
|
|
403
407
|
const lowerLocation = location.toLowerCase()
|
|
@@ -423,7 +427,7 @@ export const createCommand = new Command('create')
|
|
|
423
427
|
.argument('[name]', 'Container name')
|
|
424
428
|
.option(
|
|
425
429
|
'-e, --engine <engine>',
|
|
426
|
-
'Database engine (postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch)',
|
|
430
|
+
'Database engine (postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb, cockroachdb, surrealdb, questdb, typedb, influxdb)',
|
|
427
431
|
)
|
|
428
432
|
.option('--db-version <version>', 'Database version (e.g., 17, 8.0)')
|
|
429
433
|
.option('-d, --database <database>', 'Database name')
|
package/cli/commands/engines.ts
CHANGED
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
type InstalledQdrantEngine,
|
|
45
45
|
type InstalledMeilisearchEngine,
|
|
46
46
|
type InstalledCouchDBEngine,
|
|
47
|
+
type InstalledCockroachDBEngine,
|
|
48
|
+
type InstalledSurrealDBEngine,
|
|
49
|
+
type InstalledQuestDBEngine,
|
|
50
|
+
type InstalledTypeDBEngine,
|
|
51
|
+
type InstalledInfluxDBEngine,
|
|
47
52
|
} from '../helpers'
|
|
48
53
|
import { Engine, Platform } from '../../types'
|
|
49
54
|
import {
|
|
@@ -65,6 +70,8 @@ import { couchdbBinaryManager } from '../../engines/couchdb/binary-manager'
|
|
|
65
70
|
import { cockroachdbBinaryManager } from '../../engines/cockroachdb/binary-manager'
|
|
66
71
|
import { surrealdbBinaryManager } from '../../engines/surrealdb/binary-manager'
|
|
67
72
|
import { questdbBinaryManager } from '../../engines/questdb/binary-manager'
|
|
73
|
+
import { typedbBinaryManager } from '../../engines/typedb/binary-manager'
|
|
74
|
+
import { influxdbBinaryManager } from '../../engines/influxdb/binary-manager'
|
|
68
75
|
import {
|
|
69
76
|
DEFAULT_DOCUMENTDB_VERSION,
|
|
70
77
|
normalizeDocumentDBVersion,
|
|
@@ -458,6 +465,21 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
458
465
|
const couchdbEngines = engines.filter(
|
|
459
466
|
(e): e is InstalledCouchDBEngine => e.engine === 'couchdb',
|
|
460
467
|
)
|
|
468
|
+
const cockroachdbEngines = engines.filter(
|
|
469
|
+
(e): e is InstalledCockroachDBEngine => e.engine === 'cockroachdb',
|
|
470
|
+
)
|
|
471
|
+
const surrealdbEngines = engines.filter(
|
|
472
|
+
(e): e is InstalledSurrealDBEngine => e.engine === 'surrealdb',
|
|
473
|
+
)
|
|
474
|
+
const questdbEngines = engines.filter(
|
|
475
|
+
(e): e is InstalledQuestDBEngine => e.engine === 'questdb',
|
|
476
|
+
)
|
|
477
|
+
const typedbEngines = engines.filter(
|
|
478
|
+
(e): e is InstalledTypeDBEngine => e.engine === 'typedb',
|
|
479
|
+
)
|
|
480
|
+
const influxdbEngines = engines.filter(
|
|
481
|
+
(e): e is InstalledInfluxDBEngine => e.engine === 'influxdb',
|
|
482
|
+
)
|
|
461
483
|
|
|
462
484
|
// Calculate total size for PostgreSQL
|
|
463
485
|
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
@@ -626,6 +648,76 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
626
648
|
)
|
|
627
649
|
}
|
|
628
650
|
|
|
651
|
+
// CockroachDB rows
|
|
652
|
+
for (const engine of cockroachdbEngines) {
|
|
653
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
654
|
+
|
|
655
|
+
console.log(
|
|
656
|
+
chalk.gray(' ') +
|
|
657
|
+
getEngineIcon('cockroachdb') +
|
|
658
|
+
chalk.cyan('cockroachdb'.padEnd(13)) +
|
|
659
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
660
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
661
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// SurrealDB rows
|
|
666
|
+
for (const engine of surrealdbEngines) {
|
|
667
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
668
|
+
|
|
669
|
+
console.log(
|
|
670
|
+
chalk.gray(' ') +
|
|
671
|
+
getEngineIcon('surrealdb') +
|
|
672
|
+
chalk.cyan('surrealdb'.padEnd(13)) +
|
|
673
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
674
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
675
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// QuestDB rows
|
|
680
|
+
for (const engine of questdbEngines) {
|
|
681
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
682
|
+
|
|
683
|
+
console.log(
|
|
684
|
+
chalk.gray(' ') +
|
|
685
|
+
getEngineIcon('questdb') +
|
|
686
|
+
chalk.cyan('questdb'.padEnd(13)) +
|
|
687
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
688
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
689
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// TypeDB rows
|
|
694
|
+
for (const engine of typedbEngines) {
|
|
695
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
696
|
+
|
|
697
|
+
console.log(
|
|
698
|
+
chalk.gray(' ') +
|
|
699
|
+
getEngineIcon('typedb') +
|
|
700
|
+
chalk.cyan('typedb'.padEnd(13)) +
|
|
701
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
702
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
703
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// InfluxDB rows
|
|
708
|
+
for (const engine of influxdbEngines) {
|
|
709
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
710
|
+
|
|
711
|
+
console.log(
|
|
712
|
+
chalk.gray(' ') +
|
|
713
|
+
getEngineIcon('influxdb') +
|
|
714
|
+
chalk.cyan('influxdb'.padEnd(13)) +
|
|
715
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
716
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
717
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
629
721
|
console.log(chalk.gray(' ' + '─'.repeat(59)))
|
|
630
722
|
|
|
631
723
|
// Summary
|
|
@@ -735,6 +827,61 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
735
827
|
),
|
|
736
828
|
)
|
|
737
829
|
}
|
|
830
|
+
if (cockroachdbEngines.length > 0) {
|
|
831
|
+
const totalCockroachDBSize = cockroachdbEngines.reduce(
|
|
832
|
+
(acc, e) => acc + e.sizeBytes,
|
|
833
|
+
0,
|
|
834
|
+
)
|
|
835
|
+
console.log(
|
|
836
|
+
chalk.gray(
|
|
837
|
+
` CockroachDB: ${cockroachdbEngines.length} version(s), ${formatBytes(totalCockroachDBSize)}`,
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
if (surrealdbEngines.length > 0) {
|
|
842
|
+
const totalSurrealDBSize = surrealdbEngines.reduce(
|
|
843
|
+
(acc, e) => acc + e.sizeBytes,
|
|
844
|
+
0,
|
|
845
|
+
)
|
|
846
|
+
console.log(
|
|
847
|
+
chalk.gray(
|
|
848
|
+
` SurrealDB: ${surrealdbEngines.length} version(s), ${formatBytes(totalSurrealDBSize)}`,
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
if (questdbEngines.length > 0) {
|
|
853
|
+
const totalQuestDBSize = questdbEngines.reduce(
|
|
854
|
+
(acc, e) => acc + e.sizeBytes,
|
|
855
|
+
0,
|
|
856
|
+
)
|
|
857
|
+
console.log(
|
|
858
|
+
chalk.gray(
|
|
859
|
+
` QuestDB: ${questdbEngines.length} version(s), ${formatBytes(totalQuestDBSize)}`,
|
|
860
|
+
),
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
if (typedbEngines.length > 0) {
|
|
864
|
+
const totalTypeDBSize = typedbEngines.reduce(
|
|
865
|
+
(acc, e) => acc + e.sizeBytes,
|
|
866
|
+
0,
|
|
867
|
+
)
|
|
868
|
+
console.log(
|
|
869
|
+
chalk.gray(
|
|
870
|
+
` TypeDB: ${typedbEngines.length} version(s), ${formatBytes(totalTypeDBSize)}`,
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
if (influxdbEngines.length > 0) {
|
|
875
|
+
const totalInfluxDBSize = influxdbEngines.reduce(
|
|
876
|
+
(acc, e) => acc + e.sizeBytes,
|
|
877
|
+
0,
|
|
878
|
+
)
|
|
879
|
+
console.log(
|
|
880
|
+
chalk.gray(
|
|
881
|
+
` InfluxDB: ${influxdbEngines.length} version(s), ${formatBytes(totalInfluxDBSize)}`,
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
}
|
|
738
885
|
console.log()
|
|
739
886
|
}
|
|
740
887
|
|
|
@@ -1835,9 +1982,117 @@ enginesCommand
|
|
|
1835
1982
|
return
|
|
1836
1983
|
}
|
|
1837
1984
|
|
|
1985
|
+
if (['typedb', 'tdb'].includes(normalizedEngine)) {
|
|
1986
|
+
if (!version) {
|
|
1987
|
+
console.error(uiError('TypeDB requires a version (e.g., 3)'))
|
|
1988
|
+
process.exit(1)
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
const { platform, arch } = platformService.getPlatformInfo()
|
|
1992
|
+
const platformKey = `${platform}-${arch}`
|
|
1993
|
+
const supportedPlatforms = new Set([
|
|
1994
|
+
'darwin-x64',
|
|
1995
|
+
'darwin-arm64',
|
|
1996
|
+
'linux-x64',
|
|
1997
|
+
'linux-arm64',
|
|
1998
|
+
'win32-x64',
|
|
1999
|
+
])
|
|
2000
|
+
if (!supportedPlatforms.has(platformKey)) {
|
|
2001
|
+
console.error(
|
|
2002
|
+
uiError(
|
|
2003
|
+
`TypeDB binaries are not available for ${platformKey}. Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64.`,
|
|
2004
|
+
),
|
|
2005
|
+
)
|
|
2006
|
+
process.exit(1)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const engine = getEngine(Engine.TypeDB)
|
|
2010
|
+
|
|
2011
|
+
const spinner = createSpinner(`Checking TypeDB ${version} binaries...`)
|
|
2012
|
+
spinner.start()
|
|
2013
|
+
|
|
2014
|
+
let wasCached = false
|
|
2015
|
+
await engine.ensureBinaries(version, ({ stage, message }) => {
|
|
2016
|
+
if (stage === 'cached') {
|
|
2017
|
+
wasCached = true
|
|
2018
|
+
spinner.text = `TypeDB ${version} binaries ready (cached)`
|
|
2019
|
+
} else {
|
|
2020
|
+
spinner.text = message
|
|
2021
|
+
}
|
|
2022
|
+
})
|
|
2023
|
+
|
|
2024
|
+
if (wasCached) {
|
|
2025
|
+
spinner.succeed(`TypeDB ${version} binaries already installed`)
|
|
2026
|
+
} else {
|
|
2027
|
+
spinner.succeed(`TypeDB ${version} binaries downloaded`)
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Show the path for reference
|
|
2031
|
+
const { platform: typedbPlatform, arch: typedbArch } =
|
|
2032
|
+
platformService.getPlatformInfo()
|
|
2033
|
+
const typedbFullVersion = typedbBinaryManager.getFullVersion(version)
|
|
2034
|
+
const binPath = paths.getBinaryPath({
|
|
2035
|
+
engine: 'typedb',
|
|
2036
|
+
version: typedbFullVersion,
|
|
2037
|
+
platform: typedbPlatform,
|
|
2038
|
+
arch: typedbArch,
|
|
2039
|
+
})
|
|
2040
|
+
console.log(chalk.gray(` Location: ${binPath}`))
|
|
2041
|
+
|
|
2042
|
+
// Skip client tools check - TypeDB console is bundled
|
|
2043
|
+
return
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (['influxdb', 'influx'].includes(normalizedEngine)) {
|
|
2047
|
+
if (!version) {
|
|
2048
|
+
console.error(uiError('InfluxDB requires a version (e.g., 3)'))
|
|
2049
|
+
process.exit(1)
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const engine = getEngine(Engine.InfluxDB)
|
|
2053
|
+
|
|
2054
|
+
const spinner = createSpinner(
|
|
2055
|
+
`Checking InfluxDB ${version} binaries...`,
|
|
2056
|
+
)
|
|
2057
|
+
spinner.start()
|
|
2058
|
+
|
|
2059
|
+
let wasCached = false
|
|
2060
|
+
await engine.ensureBinaries(version, ({ stage, message }) => {
|
|
2061
|
+
if (stage === 'cached') {
|
|
2062
|
+
wasCached = true
|
|
2063
|
+
spinner.text = `InfluxDB ${version} binaries ready (cached)`
|
|
2064
|
+
} else {
|
|
2065
|
+
spinner.text = message
|
|
2066
|
+
}
|
|
2067
|
+
})
|
|
2068
|
+
|
|
2069
|
+
if (wasCached) {
|
|
2070
|
+
spinner.succeed(`InfluxDB ${version} binaries already installed`)
|
|
2071
|
+
} else {
|
|
2072
|
+
spinner.succeed(`InfluxDB ${version} binaries downloaded`)
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Show the path for reference
|
|
2076
|
+
const { platform: influxdbPlatform, arch: influxdbArch } =
|
|
2077
|
+
platformService.getPlatformInfo()
|
|
2078
|
+
const influxdbFullVersion =
|
|
2079
|
+
influxdbBinaryManager.getFullVersion(version)
|
|
2080
|
+
const binPath = paths.getBinaryPath({
|
|
2081
|
+
engine: 'influxdb',
|
|
2082
|
+
version: influxdbFullVersion,
|
|
2083
|
+
platform: influxdbPlatform,
|
|
2084
|
+
arch: influxdbArch,
|
|
2085
|
+
})
|
|
2086
|
+
console.log(chalk.gray(` Location: ${binPath}`))
|
|
2087
|
+
|
|
2088
|
+
// Skip client tools check for InfluxDB - it's a REST API server
|
|
2089
|
+
// with no CLI client tools (uses HTTP protocols instead)
|
|
2090
|
+
return
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1838
2093
|
console.error(
|
|
1839
2094
|
uiError(
|
|
1840
|
-
`Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb, cockroachdb, surrealdb, questdb`,
|
|
2095
|
+
`Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb, cockroachdb, surrealdb, questdb, typedb, influxdb`,
|
|
1841
2096
|
),
|
|
1842
2097
|
)
|
|
1843
2098
|
process.exit(1)
|
|
@@ -51,6 +51,7 @@ import { getEngineIcon, getPageSize } from '../../constants'
|
|
|
51
51
|
import { Engine, assertExhaustive } from '../../../types'
|
|
52
52
|
import { pressEnterToContinue } from './shared'
|
|
53
53
|
import { SpinDBError, ErrorCodes } from '../../../core/error-handler'
|
|
54
|
+
import { validateTypedbConnectionString } from './validators'
|
|
54
55
|
|
|
55
56
|
// Strip surrounding quotes from paths (handles drag-and-drop paths)
|
|
56
57
|
function stripQuotes(path: string): string {
|
|
@@ -207,6 +208,21 @@ function validateConnectionString(
|
|
|
207
208
|
return 'Connection string must start with postgresql:// or postgres://'
|
|
208
209
|
}
|
|
209
210
|
break
|
|
211
|
+
case Engine.TypeDB:
|
|
212
|
+
{
|
|
213
|
+
const typedbError = validateTypedbConnectionString(input)
|
|
214
|
+
if (typedbError) return typedbError
|
|
215
|
+
}
|
|
216
|
+
break
|
|
217
|
+
case Engine.InfluxDB:
|
|
218
|
+
if (
|
|
219
|
+
!input.startsWith('influxdb://') &&
|
|
220
|
+
!input.startsWith('http://') &&
|
|
221
|
+
!input.startsWith('https://')
|
|
222
|
+
) {
|
|
223
|
+
return 'Connection string must start with influxdb://, http://, or https://'
|
|
224
|
+
}
|
|
225
|
+
break
|
|
210
226
|
case Engine.SQLite:
|
|
211
227
|
case Engine.DuckDB:
|
|
212
228
|
return 'File-based engines do not support remote connection strings'
|
|
@@ -17,9 +17,11 @@ import { platformService } from '../../../core/platform-service'
|
|
|
17
17
|
import { portManager } from '../../../core/port-manager'
|
|
18
18
|
import { processManager } from '../../../core/process-manager'
|
|
19
19
|
import { getEngine } from '../../../engines'
|
|
20
|
+
import { BaseEngine } from '../../../engines/base-engine'
|
|
20
21
|
import { sqliteRegistry } from '../../../engines/sqlite/registry'
|
|
21
22
|
import { duckdbRegistry } from '../../../engines/duckdb/registry'
|
|
22
23
|
import { defaults } from '../../../config/defaults'
|
|
24
|
+
import { getEngineConfig } from '../../../config/engines-registry'
|
|
23
25
|
import { getPageSize } from '../../constants'
|
|
24
26
|
import { paths } from '../../../config/paths'
|
|
25
27
|
import {
|
|
@@ -52,6 +54,16 @@ import {
|
|
|
52
54
|
box,
|
|
53
55
|
} from '../../ui/theme'
|
|
54
56
|
import { handleOpenShell, handleCopyConnectionString } from './shell-handlers'
|
|
57
|
+
import { generatePassword } from '../../../core/credential-generator'
|
|
58
|
+
import {
|
|
59
|
+
saveCredentials,
|
|
60
|
+
credentialsExist,
|
|
61
|
+
getDefaultUsername,
|
|
62
|
+
} from '../../../core/credential-manager'
|
|
63
|
+
import {
|
|
64
|
+
UnsupportedOperationError,
|
|
65
|
+
isValidUsername,
|
|
66
|
+
} from '../../../core/error-handler'
|
|
55
67
|
import { handleRunSql, handleViewLogs } from './sql-handlers'
|
|
56
68
|
import {
|
|
57
69
|
handleBackupForContainer,
|
|
@@ -121,6 +133,8 @@ export async function handleCreate(): Promise<'main' | string | void> {
|
|
|
121
133
|
database = '0'
|
|
122
134
|
} else if (engine === 'qdrant' || engine === 'meilisearch') {
|
|
123
135
|
database = 'default'
|
|
136
|
+
} else if (engine === 'influxdb') {
|
|
137
|
+
database = 'mydb'
|
|
124
138
|
} else {
|
|
125
139
|
database = await promptDatabaseName(name, engine)
|
|
126
140
|
}
|
|
@@ -525,8 +539,11 @@ export async function handleList(
|
|
|
525
539
|
// (padEnd counts code points, not visual width)
|
|
526
540
|
const icon = getEngineIcon(c.engine)
|
|
527
541
|
const engineName = c.engine.padEnd(COL_ENGINE)
|
|
542
|
+
const isRunning = c.status === 'running'
|
|
528
543
|
const row =
|
|
529
|
-
|
|
544
|
+
(isRunning
|
|
545
|
+
? chalk.cyan.bold(displayName.padEnd(COL_NAME))
|
|
546
|
+
: chalk.cyan(displayName.padEnd(COL_NAME))) +
|
|
530
547
|
chalk.white(`${icon}${engineName}`) +
|
|
531
548
|
chalk.yellow(c.version.padEnd(COL_VERSION)) +
|
|
532
549
|
chalk.green(portDisplay.padEnd(COL_PORT)) +
|
|
@@ -796,22 +813,11 @@ export async function showContainerSubmenu(
|
|
|
796
813
|
: disabledItem('>', 'Open shell'),
|
|
797
814
|
)
|
|
798
815
|
|
|
799
|
-
// Run
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
config.engine !== Engine.CouchDB
|
|
805
|
-
) {
|
|
806
|
-
// Engine-specific terminology: Redis/Valkey use commands, MongoDB/FerretDB use scripts, SurrealDB uses SurrealQL, others use SQL
|
|
807
|
-
const runScriptLabel =
|
|
808
|
-
config.engine === Engine.Redis || config.engine === Engine.Valkey
|
|
809
|
-
? 'Run command file'
|
|
810
|
-
: config.engine === Engine.MongoDB || config.engine === Engine.FerretDB
|
|
811
|
-
? 'Run script file'
|
|
812
|
-
: config.engine === Engine.SurrealDB
|
|
813
|
-
? 'Run SurrealQL file'
|
|
814
|
-
: 'Run SQL file'
|
|
816
|
+
// Run script file - requires database selection for multi-db containers
|
|
817
|
+
// Label comes from engines.json scriptFileLabel; null means no script support (REST API engines)
|
|
818
|
+
const engineConfig = await getEngineConfig(config.engine)
|
|
819
|
+
if (engineConfig.scriptFileLabel) {
|
|
820
|
+
const runScriptLabel = engineConfig.scriptFileLabel
|
|
815
821
|
actionChoices.push(
|
|
816
822
|
canDoDbAction
|
|
817
823
|
? { name: `${chalk.yellow('▷')} ${runScriptLabel}`, value: 'run-sql' }
|
|
@@ -826,6 +832,20 @@ export async function showContainerSubmenu(
|
|
|
826
832
|
: disabledItem('⎘', 'Copy connection string'),
|
|
827
833
|
)
|
|
828
834
|
|
|
835
|
+
// Create user - only for engines that override createUser from BaseEngine
|
|
836
|
+
const engine = getEngine(config.engine)
|
|
837
|
+
const supportsUsers = engine.createUser !== BaseEngine.prototype.createUser
|
|
838
|
+
if (supportsUsers) {
|
|
839
|
+
actionChoices.push(
|
|
840
|
+
containerReady
|
|
841
|
+
? {
|
|
842
|
+
name: `${chalk.yellow('+')} Create user`,
|
|
843
|
+
value: 'create_user',
|
|
844
|
+
}
|
|
845
|
+
: disabledItem('+', 'Create user'),
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
829
849
|
// Backup - requires database selection for multi-db containers
|
|
830
850
|
actionChoices.push(
|
|
831
851
|
canDoDbAction
|
|
@@ -903,6 +923,7 @@ export async function showContainerSubmenu(
|
|
|
903
923
|
name: `${chalk.blue('⌂')} Back to main menu ${chalk.gray('(esc)')}`,
|
|
904
924
|
value: 'main',
|
|
905
925
|
},
|
|
926
|
+
new inquirer.Separator(),
|
|
906
927
|
)
|
|
907
928
|
|
|
908
929
|
const { action } = await escapeablePrompt<{ action: string }>([
|
|
@@ -991,6 +1012,10 @@ export async function showContainerSubmenu(
|
|
|
991
1012
|
await handleCopyConnectionString(containerName, activeDatabase)
|
|
992
1013
|
await showContainerSubmenu(containerName, showMainMenu, activeDatabase)
|
|
993
1014
|
return
|
|
1015
|
+
case 'create_user':
|
|
1016
|
+
await handleCreateUser(containerName, activeDatabase)
|
|
1017
|
+
await showContainerSubmenu(containerName, showMainMenu, activeDatabase)
|
|
1018
|
+
return
|
|
994
1019
|
case 'backup':
|
|
995
1020
|
await handleBackupForContainer(containerName, activeDatabase)
|
|
996
1021
|
await showContainerSubmenu(containerName, showMainMenu, activeDatabase)
|
|
@@ -2090,3 +2115,131 @@ async function handleExportDocker(
|
|
|
2090
2115
|
await pressEnterToContinue()
|
|
2091
2116
|
await showContainerSubmenu(containerName, showMainMenu, undefined)
|
|
2092
2117
|
}
|
|
2118
|
+
|
|
2119
|
+
async function handleCreateUser(
|
|
2120
|
+
containerName: string,
|
|
2121
|
+
activeDatabase?: string,
|
|
2122
|
+
): Promise<void> {
|
|
2123
|
+
const config = await containerManager.getConfig(containerName)
|
|
2124
|
+
if (!config) {
|
|
2125
|
+
console.log(uiError(`Container "${containerName}" not found`))
|
|
2126
|
+
await pressEnterToContinue()
|
|
2127
|
+
return
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
try {
|
|
2131
|
+
// Prompt for username
|
|
2132
|
+
const defaultUser = getDefaultUsername(config.engine)
|
|
2133
|
+
const { username } = await escapeablePrompt<{ username: string }>([
|
|
2134
|
+
{
|
|
2135
|
+
type: 'input',
|
|
2136
|
+
name: 'username',
|
|
2137
|
+
message: 'Username:',
|
|
2138
|
+
default: defaultUser,
|
|
2139
|
+
validate: (input: string) => {
|
|
2140
|
+
if (!input.trim()) return 'Username is required'
|
|
2141
|
+
if (!isValidUsername(input)) {
|
|
2142
|
+
return 'Must start with a letter, contain only letters/numbers/underscores'
|
|
2143
|
+
}
|
|
2144
|
+
return true
|
|
2145
|
+
},
|
|
2146
|
+
},
|
|
2147
|
+
])
|
|
2148
|
+
|
|
2149
|
+
// Check for existing credentials
|
|
2150
|
+
if (credentialsExist(containerName, config.engine, username)) {
|
|
2151
|
+
const overwrite = await promptConfirm(
|
|
2152
|
+
`Credentials for "${username}" already exist. Overwrite?`,
|
|
2153
|
+
false,
|
|
2154
|
+
)
|
|
2155
|
+
if (!overwrite) {
|
|
2156
|
+
console.log(chalk.yellow('Credential creation cancelled.'))
|
|
2157
|
+
await pressEnterToContinue()
|
|
2158
|
+
return
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const password = generatePassword({ length: 20, alphanumericOnly: true })
|
|
2163
|
+
const engine = getEngine(config.engine)
|
|
2164
|
+
|
|
2165
|
+
const spinner = createSpinner(`Creating user "${username}"...`)
|
|
2166
|
+
spinner.start()
|
|
2167
|
+
|
|
2168
|
+
let credentials
|
|
2169
|
+
try {
|
|
2170
|
+
credentials = await engine.createUser(config, {
|
|
2171
|
+
username,
|
|
2172
|
+
password,
|
|
2173
|
+
database: activeDatabase || config.database,
|
|
2174
|
+
})
|
|
2175
|
+
spinner.succeed(`Created user "${username}"`)
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
spinner.fail(`Failed to create user "${username}"`)
|
|
2178
|
+
throw error
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Save credentials (non-fatal — credentials are already created)
|
|
2182
|
+
let credentialFile: string | undefined
|
|
2183
|
+
try {
|
|
2184
|
+
credentialFile = await saveCredentials(
|
|
2185
|
+
containerName,
|
|
2186
|
+
config.engine,
|
|
2187
|
+
credentials,
|
|
2188
|
+
)
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
console.log(
|
|
2191
|
+
uiWarning(
|
|
2192
|
+
`Could not save credentials to disk: ${(error as Error).message}`,
|
|
2193
|
+
),
|
|
2194
|
+
)
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
console.log()
|
|
2198
|
+
if (credentials.apiKey) {
|
|
2199
|
+
console.log(` ${chalk.gray('Key name:')} ${credentials.username}`)
|
|
2200
|
+
console.log(` ${chalk.gray('API key:')} ${credentials.apiKey}`)
|
|
2201
|
+
console.log(
|
|
2202
|
+
` ${chalk.gray('API URL:')} ${credentials.connectionString}`,
|
|
2203
|
+
)
|
|
2204
|
+
} else {
|
|
2205
|
+
console.log(` ${chalk.gray('Username:')} ${credentials.username}`)
|
|
2206
|
+
console.log(` ${chalk.gray('Password:')} ${credentials.password}`)
|
|
2207
|
+
if (credentials.database) {
|
|
2208
|
+
console.log(` ${chalk.gray('Database:')} ${credentials.database}`)
|
|
2209
|
+
}
|
|
2210
|
+
console.log(
|
|
2211
|
+
` ${chalk.gray('URL:')} ${credentials.connectionString}`,
|
|
2212
|
+
)
|
|
2213
|
+
}
|
|
2214
|
+
if (credentialFile) {
|
|
2215
|
+
console.log()
|
|
2216
|
+
console.log(` ${chalk.gray('Saved to:')} ${credentialFile}`)
|
|
2217
|
+
}
|
|
2218
|
+
console.log()
|
|
2219
|
+
|
|
2220
|
+
// Offer to copy to clipboard
|
|
2221
|
+
try {
|
|
2222
|
+
const copyText = credentials.apiKey || credentials.connectionString
|
|
2223
|
+
const copied = await platformService.copyToClipboard(copyText)
|
|
2224
|
+
if (copied) {
|
|
2225
|
+
console.log(
|
|
2226
|
+
uiSuccess(
|
|
2227
|
+
credentials.apiKey
|
|
2228
|
+
? 'API key copied to clipboard'
|
|
2229
|
+
: 'Connection string copied to clipboard',
|
|
2230
|
+
),
|
|
2231
|
+
)
|
|
2232
|
+
}
|
|
2233
|
+
} catch {
|
|
2234
|
+
// Clipboard failure is non-critical — credentials are already displayed above
|
|
2235
|
+
}
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
if (error instanceof UnsupportedOperationError) {
|
|
2238
|
+
console.log(uiError('User management is not supported for this engine'))
|
|
2239
|
+
} else {
|
|
2240
|
+
console.log(uiError((error as Error).message))
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
await pressEnterToContinue()
|
|
2245
|
+
}
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
type InstalledCockroachDBEngine,
|
|
31
31
|
type InstalledSurrealDBEngine,
|
|
32
32
|
type InstalledQuestDBEngine,
|
|
33
|
+
type InstalledTypeDBEngine,
|
|
34
|
+
type InstalledInfluxDBEngine,
|
|
33
35
|
} from '../../helpers'
|
|
34
36
|
|
|
35
37
|
import { type MenuChoice } from './shared'
|
|
@@ -95,6 +97,10 @@ export async function handleEngines(): Promise<void> {
|
|
|
95
97
|
...engines.filter(
|
|
96
98
|
(e): e is InstalledQuestDBEngine => e.engine === 'questdb',
|
|
97
99
|
),
|
|
100
|
+
...engines.filter((e): e is InstalledTypeDBEngine => e.engine === 'typedb'),
|
|
101
|
+
...engines.filter(
|
|
102
|
+
(e): e is InstalledInfluxDBEngine => e.engine === 'influxdb',
|
|
103
|
+
),
|
|
98
104
|
]
|
|
99
105
|
|
|
100
106
|
// Calculate total size
|
|
@@ -46,6 +46,8 @@ function generatePreviewLine(mode: IconMode): string {
|
|
|
46
46
|
[Engine.CockroachDB]: '[CR]',
|
|
47
47
|
[Engine.SurrealDB]: '[SR]',
|
|
48
48
|
[Engine.QuestDB]: '[QS]',
|
|
49
|
+
[Engine.TypeDB]: '[TB]',
|
|
50
|
+
[Engine.InfluxDB]: '[IX]',
|
|
49
51
|
}
|
|
50
52
|
const icons = PREVIEW_ENGINES.map((engine) => {
|
|
51
53
|
const icon = ASCII_ICONS[engine] || '[??]'
|
|
@@ -73,6 +75,8 @@ function generatePreviewLine(mode: IconMode): string {
|
|
|
73
75
|
[Engine.CockroachDB]: '\ue269',
|
|
74
76
|
[Engine.SurrealDB]: '\uedfe',
|
|
75
77
|
[Engine.QuestDB]: '\ued2f',
|
|
78
|
+
[Engine.TypeDB]: '\ue706',
|
|
79
|
+
[Engine.InfluxDB]: '\udb85\udf95',
|
|
76
80
|
}
|
|
77
81
|
const icons = PREVIEW_ENGINES.map((engine) => {
|
|
78
82
|
const icon = NERD_ICONS[engine] || '\ue706'
|
|
@@ -100,6 +104,8 @@ function generatePreviewLine(mode: IconMode): string {
|
|
|
100
104
|
[Engine.CockroachDB]: '\u{1FAB3}',
|
|
101
105
|
[Engine.SurrealDB]: '\u{1F300}',
|
|
102
106
|
[Engine.QuestDB]: '\u23F1',
|
|
107
|
+
[Engine.TypeDB]: '\u{1F916}',
|
|
108
|
+
[Engine.InfluxDB]: '\u{1F4C8}',
|
|
103
109
|
}
|
|
104
110
|
const icons = PREVIEW_ENGINES.map((engine) => EMOJI_ICONS[engine] || '\u25A3')
|
|
105
111
|
return icons.join(' ')
|