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.
Files changed (64) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +107 -826
  3. package/cli/commands/create.ts +5 -1
  4. package/cli/commands/engines.ts +256 -1
  5. package/cli/commands/menu/backup-handlers.ts +16 -0
  6. package/cli/commands/menu/container-handlers.ts +170 -17
  7. package/cli/commands/menu/engine-handlers.ts +6 -0
  8. package/cli/commands/menu/settings-handlers.ts +6 -0
  9. package/cli/commands/menu/shell-handlers.ts +74 -14
  10. package/cli/commands/menu/sql-handlers.ts +8 -50
  11. package/cli/commands/menu/validators.ts +8 -0
  12. package/cli/commands/users.ts +264 -0
  13. package/cli/constants.ts +8 -0
  14. package/cli/helpers.ts +140 -0
  15. package/cli/index.ts +2 -0
  16. package/cli/ui/prompts.ts +24 -20
  17. package/config/backup-formats.ts +28 -0
  18. package/config/engine-defaults.ts +26 -0
  19. package/config/engines-registry.ts +1 -0
  20. package/config/engines.json +50 -0
  21. package/config/engines.schema.json +6 -1
  22. package/core/base-binary-manager.ts +6 -1
  23. package/core/config-manager.ts +20 -0
  24. package/core/credential-manager.ts +257 -0
  25. package/core/dependency-manager.ts +5 -0
  26. package/core/docker-exporter.ts +30 -0
  27. package/core/error-handler.ts +19 -0
  28. package/engines/base-engine.ts +32 -1
  29. package/engines/clickhouse/index.ts +99 -3
  30. package/engines/cockroachdb/index.ts +69 -2
  31. package/engines/couchdb/index.ts +149 -1
  32. package/engines/ferretdb/README.md +4 -0
  33. package/engines/ferretdb/index.ts +342 -13
  34. package/engines/index.ts +8 -0
  35. package/engines/influxdb/README.md +180 -0
  36. package/engines/influxdb/api-client.ts +64 -0
  37. package/engines/influxdb/backup.ts +160 -0
  38. package/engines/influxdb/binary-manager.ts +110 -0
  39. package/engines/influxdb/binary-urls.ts +69 -0
  40. package/engines/influxdb/hostdb-releases.ts +23 -0
  41. package/engines/influxdb/index.ts +1227 -0
  42. package/engines/influxdb/restore.ts +417 -0
  43. package/engines/influxdb/version-maps.ts +75 -0
  44. package/engines/influxdb/version-validator.ts +128 -0
  45. package/engines/mariadb/index.ts +96 -1
  46. package/engines/meilisearch/index.ts +97 -1
  47. package/engines/mongodb/index.ts +82 -0
  48. package/engines/mysql/index.ts +105 -1
  49. package/engines/postgresql/index.ts +92 -0
  50. package/engines/qdrant/index.ts +107 -2
  51. package/engines/redis/index.ts +106 -12
  52. package/engines/surrealdb/index.ts +102 -2
  53. package/engines/typedb/backup.ts +167 -0
  54. package/engines/typedb/binary-manager.ts +200 -0
  55. package/engines/typedb/binary-urls.ts +38 -0
  56. package/engines/typedb/cli-utils.ts +210 -0
  57. package/engines/typedb/hostdb-releases.ts +118 -0
  58. package/engines/typedb/index.ts +1275 -0
  59. package/engines/typedb/restore.ts +377 -0
  60. package/engines/typedb/version-maps.ts +48 -0
  61. package/engines/typedb/version-validator.ts +127 -0
  62. package/engines/valkey/index.ts +70 -2
  63. package/package.json +4 -1
  64. package/types/index.ts +37 -0
@@ -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')
@@ -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
- chalk.cyan(displayName.padEnd(COL_NAME)) +
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 SQL/script - requires database selection for multi-db containers
800
- // REST API engines (Qdrant, Meilisearch, CouchDB) don't support script files - hide the option entirely
801
- if (
802
- config.engine !== Engine.Qdrant &&
803
- config.engine !== Engine.Meilisearch &&
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(' ')